Appium Tutorial: Mobile App Testing from Setup to First Test
Appium is the most widely used framework for mobile app automation. It lets you write tests once and run them on Android and iOS using the same API — whether your app is native, hybrid, or a mobile web app.
This tutorial walks you through everything: installing Appium, setting up your first test, and running it on a real device or emulator.
What Is Appium?
Appium is an open-source test automation framework for mobile apps. It uses the WebDriver protocol (the same standard as Selenium) and talks to mobile platforms through vendor-specific drivers:
- UiAutomator2 — Android native apps
- XCUITest — iOS native apps
- Espresso — Android (faster, in-process)
- Gecko — Mobile browsers
The key benefit: your test code doesn't care whether it's running on Android or iOS. You interact with elements, tap buttons, fill forms, and assert states using the same commands.
Appium runs as a server process. Your test code is a client that sends WebDriver commands to the Appium server, which forwards them to the device.
Test code → Appium Server → UiAutomator2/XCUITest → Device/EmulatorPrerequisites
Before installing Appium, you need:
For Android:
- Java 11+ (JDK)
- Android Studio + Android SDK
ANDROID_HOMEenvironment variable set- An AVD (Android Virtual Device) or physical device with USB debugging enabled
For iOS:
- macOS (required — no iOS testing on Windows/Linux)
- Xcode 14+
- Xcode Command Line Tools
- A Simulator or physical device
Installing Appium
Appium 2.x is installed via npm:
npm install -g appiumInstall the driver for your target platform:
# Android
appium driver install uiautomator2
<span class="hljs-comment"># iOS
appium driver install xcuitestVerify everything is correct with the Appium doctor:
npm install -g appium-doctor
appium-doctor --android
appium-doctor --iosFix any issues appium-doctor reports before continuing. Missing SDK paths and unsigned certificates are the most common blockers.
Start the Appium server:
appium --port 4723You'll see output confirming the server is listening. Leave this running while your tests execute.
Your First Android Test (JavaScript)
We'll use WebdriverIO — the most popular Appium client for JavaScript/TypeScript:
mkdir appium-demo && <span class="hljs-built_in">cd appium-demo
npm init -y
npm install --save-dev webdriverio @wdio/cliCreate wdio.conf.js:
exports.config = {
runner: 'local',
port: 4723,
specs: ['./test/specs/**/*.js'],
capabilities: [{
platformName: 'Android',
'appium:deviceName': 'Pixel_4_API_30',
'appium:app': '/path/to/your/app.apk',
'appium:automationName': 'UiAutomator2',
'appium:newCommandTimeout': 240,
}],
framework: 'mocha',
reporters: ['spec'],
mochaOpts: {
timeout: 60000,
},
};Create your first test at test/specs/login.spec.js:
describe('Login flow', () => {
it('should login with valid credentials', async () => {
// Find the username field by accessibility ID
const usernameField = await $('~username');
await usernameField.setValue('testuser@example.com');
// Find the password field
const passwordField = await $('~password');
await passwordField.setValue('securepassword');
// Tap the login button
const loginButton = await $('~loginButton');
await loginButton.click();
// Assert the home screen is visible
const homeHeader = await $('~homeHeader');
await expect(homeHeader).toBeDisplayed();
});
it('should show error for invalid credentials', async () => {
const usernameField = await $('~username');
await usernameField.setValue('wrong@example.com');
const passwordField = await $('~password');
await passwordField.setValue('wrongpassword');
const loginButton = await $('~loginButton');
await loginButton.click();
const errorMessage = await $('~errorMessage');
await expect(errorMessage).toBeDisplayed();
await expect(errorMessage).toHaveText('Invalid credentials');
});
});Run your tests:
npx wdio run wdio.conf.jsFinding Elements
Appium supports several element locator strategies:
// Accessibility ID (recommended — works on both platforms)
const element = await $('~myAccessibilityId');
// XPath (flexible but slow)
const element = await $('//android.widget.TextView[@text="Submit"]');
// Class name
const element = await $('.android.widget.Button');
// Android UiAutomator
const element = await $('android=new UiSelector().text("Login")');
// iOS NSPredicate string
const element = await $('ios=label == "Login"');
// ID
const element = await $('#com.example.app:id/login_button');Use the Appium Inspector to visually browse your app's element hierarchy:
npm install -g appium-inspector
appium-inspectorConnect to your running Appium server and launch your app. You'll see the full UI tree and can click elements to get their locators.
Gestures and Interactions
Mobile testing often requires touch gestures beyond simple taps:
// Scroll down
await driver.execute('mobile: scroll', {
direction: 'down',
});
// Swipe left on an element
await driver.execute('mobile: swipe', {
direction: 'left',
element: (await $('~cardElement')).elementId,
});
// Long press
await driver.execute('mobile: longClickGesture', {
element: (await $('~holdButton')).elementId,
duration: 2000,
});
// Pinch to zoom
await driver.execute('mobile: pinchOpenGesture', {
elementId: (await $('~mapView')).elementId,
scale: 2.0,
velocity: 2.2,
});
// Type text (alternative to setValue)
await driver.execute('mobile: type', { text: 'Hello World' });Cross-Platform Test Strategy
The real power of Appium is writing tests that run on both Android and iOS. Here's a pattern for managing platform differences:
// helpers/platform.js
const isIOS = () => driver.isIOS;
const isAndroid = () => driver.isAndroid;
async function findElement(androidSelector, iosSelector) {
if (isIOS()) {
return $(`ios=${iosSelector}`);
}
return $(`android=${androidSelector}`);
}
module.exports = { isIOS, isAndroid, findElement };Use accessibility IDs consistently in your app code — they work across platforms with the same locator:
// Both Android and iOS
const submitButton = await $('~submitButton');
await submitButton.click();Work with your mobile developers to add accessibilityLabel (iOS) and contentDescription (Android) to interactive elements from the start. Retrofitting them later is painful.
Handling Async Wait
Mobile apps load data asynchronously. Use waitForDisplayed instead of assuming elements are immediately present:
// Wait up to 10 seconds for an element
const dashboard = await $('~dashboardScreen');
await dashboard.waitForDisplayed({ timeout: 10000 });
// Wait for an element to disappear (e.g., loading spinner)
const spinner = await $('~loadingSpinner');
await spinner.waitForDisplayed({ timeout: 5000, reverse: true });
// Custom polling
await browser.waitUntil(
async () => {
const items = await $$('~listItem');
return items.length > 0;
},
{ timeout: 15000, interval: 500, timeoutMsg: 'List items never appeared' }
);Running on Real Devices
For Android, enable USB debugging and connect via USB:
adb devices # Confirm device appearsUpdate your capabilities:
capabilities: [{
platformName: 'Android',
'appium:deviceName': 'your-device-serial',
'appium:udid': 'your-device-serial',
'appium:app': '/path/to/app.apk',
'appium:automationName': 'UiAutomator2',
}]For iOS real devices, you need:
- A valid signing certificate and provisioning profile
- The UDID of your device (find it in Xcode → Devices)
xcrun xctrace list devicesto list connected devices
Common Appium Issues
App not found: Use absolute paths for the app capability. Relative paths frequently fail depending on how you launch Appium.
Element not found: Add waits. Appium doesn't automatically wait for elements — they must exist in the DOM at the time of the selector call.
Session creation failed: Check that appium-doctor shows all green. The most common cause is a missing ANDROID_HOME or unsigned iOS certificate.
Tests are slow: Appium starts a new session for each test suite by default. Use noReset: true and session reuse patterns to avoid reinstalling the app on every run.
Flaky tests: Add implicitWait as a global fallback, but rely on explicit waits for specific elements that take time to appear.
Scaling with CI
Add Appium tests to your CI pipeline with GitHub Actions:
# .github/workflows/appium.yml
name: Appium Android Tests
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '11'
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Start Android emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 30
script: |
npm install
appium &
sleep 5
npx wdio run wdio.conf.jsBeyond Manual Automation
Appium handles functional regression well — verifying that user flows work correctly after each release. But mobile apps also need monitoring after deployment: do real users on real devices experience crashes or broken flows?
HelpMeTest lets you write mobile test scenarios in plain English, run them on schedule, and get alerts when something breaks. No Appium server to maintain, no device farm to configure. It complements your Appium suite by catching production regressions between releases.
Summary
- Install:
npm install -g appium, thenappium driver install uiautomator2 - Start server:
appium --port 4723 - Write tests: WebdriverIO or any WebDriver client
- Find elements: Prefer accessibility IDs for cross-platform compatibility
- Handle async: Use
waitForDisplayedandwaitUntil— never assume elements are instantly present - CI: Use
reactivecircus/android-emulator-runnerfor Android emulator in GitHub Actions
Appium has a learning curve, but once it's running, you have a powerful cross-platform mobile test suite that grows with your app.