Appium 2.x Mobile Test Automation: Setup, WebdriverIO, and Real Device Testing
Appium 2.x is a significant architectural departure from Appium 1.x — drivers and plugins are now installed separately, the server is leaner, and WebdriverIO has emerged as the best client library for writing and running tests. This guide covers the complete setup for iOS and Android, element locator discovery with Appium Inspector, real device configuration, and parallel execution in CI.
Key Takeaways
Install drivers explicitly in Appium 2.x — they no longer ship with the server. Run appium driver install xcuitest and appium driver install uiautomator2 after installing the Appium server, or tests will fail to start with a cryptic "no driver" error. WebdriverIO's @wdio/appium-service starts and stops Appium automatically. You don't need to manage the Appium server process manually in local development or CI — the service integration handles it. Appium Inspector is the fastest way to find reliable locators. Connect it to a running Appium session, click elements, and copy the generated locator strategy rather than guessing accessibility IDs from source code. Real devices require provisioning profiles on iOS and developer mode on Android 12+. Both are one-time setup steps, but skipping them produces confusing authorization errors rather than clear failure messages. Use accessibility IDs as your primary locator strategy. They map to accessibilityIdentifier on iOS and contentDescription on Android — cross-platform, stable across refactors, and readable in test code.
Appium remains the most widely adopted mobile automation framework in enterprise environments, primarily because it supports native apps, hybrid apps, and mobile web from a single API, works with any language through WebDriver protocol clients, and requires no modification to your app's source code. Appium 2.x modernizes the architecture significantly while keeping the core WebDriver protocol unchanged.
What Changed in Appium 2.x
Appium 1.x shipped as a monolithic server that included all platform drivers. Appium 2.x separates concerns:
- Appium server — the WebDriver protocol layer only
- Drivers — platform-specific automation engines, installed separately
- Plugins — optional capabilities (image comparison, device farm routing, etc.)
This means a leaner server that updates independently of drivers, and driver teams can ship updates on their own schedule.
Installing Appium 2.x
Install the Appium server globally:
npm install -g appiumInstall the platform drivers you need:
# iOS
appium driver install xcuitest
<span class="hljs-comment"># Android
appium driver install uiautomator2Verify installed drivers:
appium driver list --installedCheck that all dependencies are satisfied using the Appium Doctor:
npm install -g appium-doctor
appium-doctor --ios
appium-doctor --androidAppium Doctor checks for Xcode, Xcode Command Line Tools, iOS Simulator, Java, Android SDK, ANDROID_HOME, and other prerequisites, listing what's missing with actionable fix instructions.
iOS Prerequisites
Appium's XCUITest driver automates iOS through Apple's XCUITest framework, the same technology Xcode uses for UI tests. Required setup:
- Xcode — install from the App Store (latest stable version recommended)
- Xcode Command Line Tools —
xcode-select --install - WebDriverAgent — built automatically by the XCUITest driver on first use
For simulator testing, no code signing is required. For real device testing, you need:
- An Apple Developer account (free tier works for development devices)
- A provisioning profile or automatic signing configured in Xcode
- The device trusted ("Trust" prompt on device when connected via USB)
# Verify simulator is available
xcrun simctl list devices <span class="hljs-pipe">| grep <span class="hljs-string">"iPhone 15"
<span class="hljs-comment"># Boot a simulator
xcrun simctl boot <span class="hljs-string">"iPhone 15"Android Prerequisites
The UiAutomator2 driver uses Android's UIAutomator2 framework. Required setup:
- Java 11+ —
java -version - Android SDK — install via Android Studio or
sdkmanager - ANDROID_HOME environment variable — points to SDK directory
- Platform tools in PATH —
adbmust be reachable
# Check connected devices
adb devices
<span class="hljs-comment"># Start an emulator
emulator -avd Pixel_6_API_33 &
<span class="hljs-comment"># Enable developer mode on physical Android 12+ device
<span class="hljs-comment"># Settings → About Phone → tap Build Number 7 times
<span class="hljs-comment"># Settings → Developer Options → USB Debugging: ONSetting Up WebdriverIO
WebdriverIO is the recommended JavaScript client for Appium 2.x. It provides a clean async API, built-in service management, and strong TypeScript support.
Initialize a new WebdriverIO project:
npm init wdio@latest .Select "Mobile - iOS/Android" when prompted. The wizard generates a configuration file. Here's a refined version:
// wdio.conf.ts
import { type Options } from '@wdio/types';
export const config: Options.Testrunner = {
runner: 'local',
specs: ['./test/specs/**/*.spec.ts'],
maxInstances: 1,
capabilities: [{
platformName: 'iOS',
'appium:automationName': 'XCUITest',
'appium:deviceName': 'iPhone 15',
'appium:platformVersion': '17.0',
'appium:app': process.env.APP_PATH || './apps/MyApp.app',
'appium:noReset': false,
}],
logLevel: 'info',
services: ['appium'],
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
ui: 'bdd',
timeout: 60000,
},
};For Android:
capabilities: [{
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': 'Pixel_6_API_33',
'appium:platformVersion': '13',
'appium:app': process.env.APP_PATH || './apps/app-debug.apk',
'appium:appPackage': 'com.example.myapp',
'appium:appActivity': 'com.example.myapp.MainActivity',
}]Writing Appium Tests with WebdriverIO
Basic Test Structure
// test/specs/login.spec.ts
describe('Login', () => {
it('should log in with valid credentials', async () => {
const emailInput = await $('~email-input'); // ~ prefix = accessibility ID
const passwordInput = await $('~password-input');
const loginButton = await $('~login-button');
await emailInput.setValue('user@example.com');
await passwordInput.setValue('secret123');
await loginButton.click();
const welcomeText = await $('~welcome-message');
await expect(welcomeText).toBeDisplayed();
});
});The ~ prefix is WebdriverIO's shorthand for the accessibility id locator strategy, which maps to:
- iOS:
accessibilityIdentifier(set via.accessibilityIdentifierin SwiftUI, ortestIDin React Native) - Android:
contentDescription
Locator Strategies
// Accessibility ID (preferred — cross-platform)
const element = await $('~my-element-id');
// XPath (fragile but universal fallback)
const element = await $('//XCUIElementTypeButton[@name="Submit"]');
// iOS class chain (iOS-specific, faster than XPath)
const element = await $('-ios class chain:**/XCUIElementTypeTextField[`label == "Email"`]');
// Android UIAutomator (Android-specific, very powerful)
const element = await $('android=new UiSelector().text("Email").className("android.widget.EditText")');
// ID (maps to resource-id on Android, accessibility identifier on iOS)
const element = await $('#com.example.myapp:id/email_input');Interacting with Elements
// Text input
await $('~search-input').setValue('query text');
await $('~search-input').clearValue();
await $('~search-input').addValue(' more text');
// Tapping
await $('~submit-button').click();
// Swiping
await driver.execute('mobile: swipe', {
direction: 'up',
element: await $('~scroll-container'),
});
// Scroll to element (iOS)
await driver.execute('mobile: scroll', {
direction: 'down',
predicateString: 'label == "Load More"',
});
// Scroll to element (Android)
await $('android=new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("Load More"))');
// Take screenshot
await driver.saveScreenshot('./screenshots/current-state.png');Waiting for Elements
WebdriverIO uses implicit waits by default. Configure the global wait timeout:
// wdio.conf.ts
waitforTimeout: 10000, // ms
waitforInterval: 500, // retry intervalFor explicit waits:
// Wait for element to appear
await $('~loading-spinner').waitForDisplayed({ timeout: 15000, reverse: true });
// Wait for element to be clickable
await $('~submit-button').waitForEnabled({ timeout: 5000 });
// Custom wait condition
await driver.waitUntil(
async () => {
const count = await $$('~list-item').length;
return count > 0;
},
{ timeout: 10000, timeoutMsg: 'No list items appeared within 10s' }
);Appium Inspector for Element Discovery
Appium Inspector is a desktop application for exploring your app's element hierarchy and testing locator strategies:
- Download from github.com/appium/appium-inspector
- Start the Appium server:
appium - Configure the desired capabilities in Inspector's UI
- Click "Start Session"
Inspector renders a screenshot of your app with an interactive element tree on the right. Click any element to see:
- Its accessibility ID, label, and class
- All available locator strategies for that element
- A "Copy" button for the generated selector
For React Native apps, Inspector is most useful for verifying that your testID props are reaching the native layer correctly. A testID on a React Native component should appear as accessibilityIdentifier on iOS and contentDescription on Android in the Inspector tree.
Real Device Testing
iOS Real Device
Configure automatic code signing in Xcode or provide explicit provisioning:
capabilities: [{
platformName: 'iOS',
'appium:automationName': 'XCUITest',
'appium:udid': 'auto', // auto-detects connected device
'appium:xcodeSigningId': 'iPhone Developer',
'appium:xcodeOrgId': 'YOUR_TEAM_ID', // from developer.apple.com
'appium:app': './apps/MyApp.ipa',
}]Find your team ID:
# List identities and their team IDs
security find-identity -v -p codesigningAndroid Real Device
capabilities: [{
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:udid': 'R38M30XXXXX', // from `adb devices`
'appium:app': './apps/app-release.apk',
}]For Android 12+ devices, enable developer mode and wireless ADB debugging to run tests without a USB cable:
adb connect 192.168.1.100:5555Parallel Execution
Run tests in parallel across multiple devices:
// wdio.conf.ts
maxInstances: 4,
capabilities: [
{
platformName: 'iOS',
'appium:udid': 'SIMULATOR_UDID_1',
// ...
},
{
platformName: 'iOS',
'appium:udid': 'SIMULATOR_UDID_2',
// ...
},
{
platformName: 'Android',
'appium:udid': 'emulator-5554',
// ...
},
{
platformName: 'Android',
'appium:udid': 'emulator-5556',
// ...
},
]Each capability object becomes a parallel worker. Use xcrun simctl list devices and adb devices to collect available UDIDs and populate the array.
CI/CD Integration
# .github/workflows/appium.yml
name: Appium Tests
on:
push:
branches: [main]
jobs:
test-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Install Appium
run: |
npm install -g appium
appium driver install uiautomator2
- name: Build APK
run: cd android && ./gradlew assembleDebug
- name: Run tests on Android Emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
arch: x86_64
script: |
APP_PATH=android/app/build/outputs/apk/debug/app-debug.apk npx wdio run wdio.android.conf.ts
- name: Upload screenshots on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: appium-screenshots
path: screenshots/Appium's language-agnostic WebDriver protocol means the test code you write today works with any future client library that speaks WebDriver. The 2.x architecture's explicit driver management adds a small upfront cost in exchange for faster driver updates, smaller server footprint, and a plugin ecosystem that extends capabilities without forking the core project. For teams testing across iOS, Android, and mobile web simultaneously, Appium remains the most complete cross-platform automation solution available.