The driver model is the load-bearing concept: Appium itself is the WebDriver protocol; per-platform automation is a driver (XCUITest driver talks to iOS, UiAutomator2 / Espresso to Android, Mac2 to macOS, Windows to Windows desktop). Client libraries exist for JavaScript, Python, Java, Ruby, and .NET (per appium-docs (opens in new window)).
When to use
If the app is iOS-only, xcuitest-suite is lighter (no external server). For React Native specifically, detox-testing is faster.
Step 1 - Install Appium server + driver
Appium 3 is current (3.9.0 on npm, 3 Nov 2026). Per migrating-2-to-3 (opens in new window) it needs Node ^20.19.0 || ^22.12.0 || >=24.0.0 and npm 10 or newer.
npm install -g appium
# Install per-platform drivers:
appium driver install xcuitest # iOS
appium driver install uiautomator2 # Android
appium driver install espresso # Android (alternative; gray-box)
appium driver install mac2 # macOS desktop
appium driver install windows # Windows desktop
# Verify:
appium driver list
appium driver doctor uiautomator2 # checks ANDROID_HOME, JDK, SDK toolsPer uia2-quickstart (opens in new window), appium driver doctor reporting "0 required fixes needed" means the Android setup is complete; it needs the Android SDK platform tools, ANDROID_HOME and a JDK with JAVA_HOME. Drivers are versioned separately from the server: after a server upgrade, run appium driver update <name> too.
Step 2 - Start the server
appium # listens on http://127.0.0.1:4723/
appium --allow-insecure=uiautomator2:adb_shell # opt-in risky featuresClients connect to http://localhost:4723 with no /wd/hub suffix (that suffix went away with Appium 2). Per migrating-2-to-3 (opens in new window), Appium 3 "makes the scope prefix mandatory" on insecure feature flags: adb_shell alone now throws; write uiautomator2:adb_shell or *:adb_shell.
Step 3 - Author a test (JavaScript example)
import { remote } from 'webdriverio';
const capabilities = {
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': 'Android Emulator',
'appium:app': '/path/to/app.apk',
};
describe('Cart flow', () => {
let driver;
before(async () => {
driver = await remote({
hostname: 'localhost',
port: 4723,
capabilities,
});
});
after(async () => {
await driver.deleteSession();
});
it('adds an item to cart', async () => {
const addButton = await driver.$('id=add_to_cart_button');
await addButton.click();
// WebdriverIO's expect retries until the text matches or times out
await expect(driver.$('id=cart_count')).toHaveText('1');
});
});The automationName capability picks the driver: UiAutomator2 (Android), XCUITest (iOS), Espresso (Android gray-box), Mac2, Windows.
Step 4 - Capabilities catalog
| Capability | Use |
|---|---|
platformName | iOS / Android / Mac / Windows |
appium:automationName | Driver name (XCUITest / UiAutomator2 / Espresso etc.) |
appium:deviceName | Logical device name (Simulator / Emulator / specific) |
appium:platformVersion | OS version (e.g. 14.0) |
appium:app | Path to .apk / .ipa |
appium:bundleId / appium:appPackage | Pre-installed app |
appium:noReset | Don't reinstall app between sessions |
appium:newCommandTimeout | Server idle timeout (default 60s) |
For device-farm runs, capabilities also include the farm's authentication tokens / device-pool selection.
Step 5 - Selector strategies (cross-platform)
Per wdio-selectors (opens in new window), "For both platforms, getting an element (or multiple elements) by their accessibility id is usually the best method." The platform-native strategies are the next choice; XPath is the last:
// Accessibility ID - same selector on iOS and Android
await driver.$('~login-button').click();
// Android: UiAutomator selector
await driver.$('android=new UiSelector().text("Cancel").className("android.widget.Button")');
// iOS: predicate string and class chain (fast, evaluated by XCUITest)
await driver.$(`-ios predicate string:type == 'XCUIElementTypeSwitch' && name CONTAINS 'Allow'`);
await driver.$('-ios class chain:**/XCUIElementTypeCell[`name BEGINSWITH "D"`]/**/XCUIElementTypeButton');
// XPath - works everywhere; slow and brittle, last resort
await driver.$('//XCUIElementTypeButton[@name="Submit"]');For a canvas or game UI with no element tree, the images plugin finds an element from a template image. Per images-plugin (opens in new window), install it with appium plugin install images and enable it at start with appium --use-plugins=images.
The cross-platform strategy: production code sets the same accessibility ID on both iOS (accessibilityIdentifier) and Android (contentDescription or resource-id). Tests use one selector for both.
Step 6 - Run
npm install --save-dev @wdio/appium-service appium@^3
# Local with one platform
WDIO_OS=android npx wdio run wdio.conf.js
# Multi-platform: WebdriverIO multi-capability config
# wdio.conf.js
exports.config = {
capabilities: [
{ platformName: 'iOS', 'appium:platformVersion': '17.4', ...iosCaps },
{ platformName: 'Android', 'appium:platformVersion': '14', ...androidCaps },
],
runner: 'local',
services: ['appium'],
};Per wdio-appium-service (opens in new window), the service "starts the Appium Server in a child process", so there is no separate server step. The same page notes that "WebdriverIO 10 requires Appium 3" and the service "refuses to launch Appium 1.x or 2.x": upgrade the server and drivers together with WebdriverIO.
Step 7 - CI integration
# Multi-platform matrix
jobs:
ui-tests:
strategy:
matrix:
platform: [iOS, Android]
runs-on: ${{ matrix.platform == 'iOS' && 'macos-15' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with: { node-version: 24 }
- run: npm ci # project pins appium@^3 + @wdio/appium-service
- run: npx appium driver install ${{ matrix.platform == 'iOS' && 'xcuitest' || 'uiautomator2' }}
- if: matrix.platform == 'Android'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
script: WDIO_OS=android npx wdio run wdio.conf.js
- if: matrix.platform == 'iOS'
run: |
xcrun simctl boot 'iPhone 15'
WDIO_OS=ios npx wdio run wdio.conf.jsUpgrading from Appium 2 to Appium 3
Per migrating-2-to-3 (opens in new window), the changes that break existing suites:
| Change | What to do |
|---|---|
| Node minimum is 20.19 (20.x), 22.12 (22.x) or any 24+, and npm 10+ | Upgrade Node on laptops and CI images first. |
| Insecure feature flags need a scope prefix | --allow-insecure=adb_shell becomes uiautomator2:adb_shell or *:adb_shell; server-scope features such as session_discovery need *:. |
Deprecated endpoints removed, e.g. GET /sessions | Use GET /appium/sessions (needs the session_discovery feature flag). |
POST /session no longer accepts desiredCapabilities / requiredCapabilities | Send W3C capabilities with the appium: vendor prefix (Step 4). Old clients that still send JSONWP payloads must be upgraded. |
| Older drivers | Run appium driver update <name> so driver versions support the new server. |
| Appium Inspector "Attach to Session" | Inspector 2025.3.1 or later. |
Upgrade order that avoids a broken half-state: Node, then appium@^3, then every driver, then the client (WebdriverIO 10 or the matching Java, Python, Ruby or .NET client), then fix flags and capabilities.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Per-platform separate test code | Defeats Appium's reuse value. | One test, multi-capability config (Step 6). |
| XPath selectors as default | Slowest selector strategy; brittle to UI tree changes. | Accessibility IDs first (Step 5). |
Hard-coded device name ('iPhone 15') in shared config | Test fails on a machine or runner where that simulator doesn't exist. | Read device name and OS version from env vars per runner, and list the available simulators (xcrun simctl list devices) in CI before booting. |
Ignoring noReset between tests | App state leaks; flaky. | noReset: false (default) reinstalls per session. |
One mega-test using before for the whole suite | Suite-long flake; one tiny issue restarts everything. | Per-test beforeEach reset; small focused tests. |
| Mixing Appium 1.x, 2.x and 3.x docs and examples | Server URL, capability format and feature-flag syntax differ; failures are cryptic. | Pin appium@^3 in package.json, follow the Appium 3 docs, migrate with the table above. |
| Server upgraded, drivers left on old versions | Sessions fail to start or commands disappear after the upgrade. | appium driver update after every server upgrade. |
Limitations
References
- Every test passes alone and the suite still fails
- Standing up device tests from zero, and the first attempt does not run anywhere
- The same cart test exists twice and the two copies have drifted
- CI has been testing Android only for four months
- Every UI tweak breaks fourteen mobile tests
- One red test, forty minutes, and a device nobody can use afterwards
- The two-factor test loses its session about half the time
- Mobile suite stopped starting sessions after the automation server upgrade
- Our "cross-platform" suite is really an Android suite
- The suite only starts a session on the laptop it was written on