Developing E2E Tests for Mobile Applications (Appium)
Appium is the only tool in mobile testing that covers iOS and Android with one test code. That's its main advantage and simultaneously the source of most problems. Behind universality lies an abstraction layer that sometimes behaves unpredictably. Writing stable Appium tests is harder than it appears.
Architecture: Appium 2 vs Appium 1
Appium 2 is not just a version, it's a different concept. Instead of monolithic server — core plus separately installed drivers:
npm install -g appium@next
appium driver install uiautomator2 # Android
appium driver install xcuitest # iOS
UIAutomator2Driver for Android, XCUITestDriver for iOS. Both support W3C WebDriver Protocol, making them compatible with WebdriverIO, Selenium Grid, and standard client libraries.
Versions we work with in 2024–2025:
- Appium 2.5+
-
appium-uiautomator2-driver3.x -
appium-xcuitest-driver7.x -
WebdriverIO8.x (JS/TS) orAppium-Python-Client4.x (Python)
Server Setup and Capabilities
The most painful place — proper Capabilities set. Wrong platformVersion or missing automationName — and session doesn't start with uninformative error.
Minimal working config for Android (WebdriverIO):
const capabilities = {
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': 'emulator-5554',
'appium:app': path.resolve('./apps/myapp.apk'),
'appium:newCommandTimeout': 240,
'appium:noReset': false,
};
For iOS add udid of device, xcodeOrgId and xcodeSigningId for real device. On simulator easier — but simulator doesn't give performance results and Push Notifications.
Page Object and Test Structure
Raw Appium code without pattern — nightmare for maintenance. Each $('//XCUIElementTypeButton[@name="Login"]') duplicates in dozens of tests, and when UI changes you fix everything at once.
Page Object with WebdriverIO:
class LoginPage {
get emailField() { return $('~email_input'); } // accessibilityId
get passwordField() { return $('~password_input'); }
get submitButton() { return $('~login_button'); }
async login(email: string, password: string) {
await this.emailField.setValue(email);
await this.passwordField.setValue(password);
await this.submitButton.click();
}
}
export default new LoginPage();
~ prefix is accessibilityId locator. Works on iOS (accessibilityIdentifier) and Android (contentDescription). Prefer it over XPath — more stable when view hierarchy changes.
Use XPath only when no alternatives: //android.widget.TextView[contains(@text,'Add')]. But long XPath chains — first cause of unstable tests.
Waits Instead of Sleep
driver.pause(3000) — anti-pattern. Replace with explicit waits:
await $('~submit_btn').waitForDisplayed({ timeout: 10000 });
await $('~success_screen').waitForExist({ timeout: 15000 });
waitForDisplayed waits for element in visible area. waitForExist — just existing in DOM. For elements appearing after animation — waitForDisplayed with { timeout: 5000, interval: 500 }.
CI Integration
Appium in CI requires running emulator or connected device. Typical scheme for GitHub Actions:
- name: Start Android Emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
script: |
appium &
sleep 5
npx wdio run wdio.conf.ts
For iOS in CI need macOS runner. Use actions/runner on macOS with Xcode pre-installed. Create simulator through xcrun simctl create.
Alternative — cloud device farms: Firebase Test Lab, BrowserStack App Automate, Sauce Labs. There emulator/device and Appium server are provided by platform.
Common Issues
StaleElementReferenceError — found element, but UI restructured before click. Wrap in retry logic: await browser.waitUntil(async () => { ... }).
Keyboard covers element — on iOS screen keyboard covers field. Before text input — await driver.hideKeyboard() or scroll to element: await element.scrollIntoView().
App doesn't respond to commands after background — 'appium:forceAppLaunch': true in capabilities or explicit driver.activateApp(bundleId).
Different locators on iOS and Android — even with accessibilityId sometimes need platform-specific locators. Solve through condition:
const selector = driver.isIOS ? '~ios_id' : '~android_id';
What's Included
- Setting up Appium 2 server and drivers for iOS and Android
- Writing tests (WebdriverIO/TypeScript or Python)
- Page Object pattern for all key screens
- CI integration (GitHub Actions / GitLab CI)
- Cloud farm execution setup on demand
- Allure or HTML reports with screenshots for each step
Timeline
5 days — basic configuration + coverage of 3–5 key flows. Full E2E coverage of large application (15–20 scenarios) — 2–3 weeks. Cost is calculated individually after app and infrastructure analysis.







