Lesson 6 of 12 · Automation Frameworks
Appium Introduction
How Appium automates native, hybrid and mobile web apps: the server and driver model, W3C capabilities, mobile locators, a first Java test and what makes mobile tests flaky.
intermediate 7 min read Updated
You will learn to
- Explain Appium's client, server and driver architecture
- Install the Appium server and the UiAutomator2 and XCUITest drivers
- Write W3C capabilities and a first Android test with the Java client
- Choose robust mobile locators and avoid common causes of flaky mobile tests
Prerequisites
- Selenium WebDriver Basics
- Java for Testers
Appium is the best-known open-source, cross-platform tool for automating mobile apps. It speaks the same W3C WebDriver protocol as Selenium, so if you know Selenium the API will feel familiar: sessions, findElement, click, explicit waits. What is different is everything around it: devices, emulators, app builds, platform-specific drivers and much more variability in timing.
This lesson covers the Appium 2 and 3 architecture, which differs significantly from Appium 1 tutorials you may still find online.
What Appium can automate
| App type | What it is | How Appium drives it |
|---|---|---|
| Native | Built with platform SDKs (Kotlin/Java, Swift) or cross-platform frameworks that render native views | Through the platform’s automation framework |
| Hybrid | Native shell with web content in a WebView | Native context for the shell, switch to a WebView context for web content |
| Mobile web | A website in the device browser | Like Selenium, with mobile Chrome or Safari |
Appium’s guiding idea is that you should not need to modify or recompile your app to test it: it automates the same build you ship, using the vendors’ own automation frameworks under the hood.
Architecture: server, drivers, clients
Test code (Java, Python, JS, ...)
| W3C WebDriver over HTTP
v
Appium server (Node.js)
|-- UiAutomator2 driver --> Android device/emulator (Google's UiAutomator2)
|-- XCUITest driver --> iOS device/simulator (Apple's XCUITest)
|-- other drivers (Espresso, Mac2, Windows, ...)
- The Appium server is a Node.js program. Since Appium 2 it ships without any drivers; you install the ones you need.
- Drivers translate WebDriver commands into platform automation. UiAutomator2 is the standard Android driver; XCUITest is the iOS driver and requires macOS with Xcode.
- Plugins extend the server (for example, image-based element finding).
- Clients are language libraries. The Java client (
io.appium:java-client) extends Selenium’s Java bindings.
Installation
You need Node.js, plus Android Studio (SDK, platform tools, an emulator) for Android, or Xcode for iOS.
npm install -g appium
appium driver install uiautomator2
appium driver install xcuitest # macOS only
appium driver list --installed
appium driver doctor uiautomator2 # checks ANDROID_HOME, Java, adb and so on
appium # starts the server on http://127.0.0.1:4723
Appium 1 served on http://localhost:4723/wd/hub. Appium 2 and later use the root path, http://127.0.0.1:4723, by default. A “404 on session creation” when migrating old code is almost always this.
Appium Inspector is a separate desktop app (also available as a web version) that starts a session, shows a screenshot of the app with its element tree, and lets you try locators. It is the mobile equivalent of browser dev tools; use it constantly.
Capabilities
Every session starts with capabilities describing the platform, device and app. Appium follows the W3C rule that non-standard capabilities need a vendor prefix, so everything except standard ones like platformName is prefixed appium::
{
"platformName": "Android",
"appium:automationName": "UiAutomator2",
"appium:deviceName": "Pixel_8_API_35",
"appium:app": "/builds/shop-app-release.apk",
"appium:appPackage": "com.example.shop",
"appium:appActivity": ".MainActivity",
"appium:noReset": false,
"appium:newCommandTimeout": 120
}
For iOS:
{
"platformName": "iOS",
"appium:automationName": "XCUITest",
"appium:deviceName": "iPhone 16",
"appium:platformVersion": "18.0",
"appium:app": "/builds/ShopApp.app",
"appium:bundleId": "com.example.shop"
}
Common ones worth knowing:
appium:app: path or URL to the.apk,.appor.ipato install.appium:udid: a specific physical device, when several are connected.appium:noResetandappium:fullReset: whether app data is cleared between sessions. Clearing gives isolation; keeping it is faster.appium:newCommandTimeout: seconds the server waits for a new command before ending the session; raise it while debugging.
A first Android test in Java
The Java client provides option classes so you do not have to type capability names by hand:
import static org.junit.jupiter.api.Assertions.assertEquals;
import io.appium.java_client.AppiumBy;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import java.net.URI;
import java.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
class LoginAndroidTest {
private AndroidDriver driver;
private WebDriverWait wait;
@BeforeEach
void setUp() throws Exception {
UiAutomator2Options options = new UiAutomator2Options()
.setDeviceName("Pixel_8_API_35")
.setApp(System.getenv("APP_PATH"))
.setNewCommandTimeout(Duration.ofSeconds(120));
driver = new AndroidDriver(URI.create("http://127.0.0.1:4723").toURL(), options);
wait = new WebDriverWait(driver, Duration.ofSeconds(15));
}
@AfterEach
void tearDown() {
if (driver != null) driver.quit();
}
@Test
void validUserSeesHome() {
wait.until(ExpectedConditions.elementToBeClickable(AppiumBy.accessibilityId("email-input")))
.sendKeys("ana@example.com");
driver.findElement(AppiumBy.accessibilityId("password-input"))
.sendKeys(System.getenv("TEST_PASSWORD"));
driver.findElement(AppiumBy.accessibilityId("login-button")).click();
var title = wait.until(ExpectedConditions.visibilityOfElementLocated(
AppiumBy.id("com.example.shop:id/home_title")));
assertEquals("Welcome back", title.getText());
}
}
UiAutomator2Options sets platformName and appium:automationName for you. The iOS equivalent is XCUITestOptions with IOSDriver.
Mobile locators
Mobile apps have no CSS and no HTML ids, so the locator strategies differ:
| Strategy | Android | iOS | Notes |
|---|---|---|---|
AppiumBy.accessibilityId |
content-description |
accessibilityIdentifier / label |
First choice: cross-platform and fast |
AppiumBy.id |
resource-id (package:id/name) |
name | Stable when developers set ids |
AppiumBy.androidUIAutomator |
UiSelector expressions | n/a | Powerful Android-only queries, including scrolling into view |
AppiumBy.iOSNsPredicateString / iOSClassChain |
n/a | Native iOS queries | Fast alternatives to XPath on iOS |
AppiumBy.xpath |
Works | Works | Slowest, especially on iOS; last resort |
The best mobile automation investment is asking developers to add accessibility identifiers (or testTags exposed as resource ids in Jetpack Compose) to key elements. It makes tests stable and it improves accessibility for screen-reader users at the same time.
// Android: scroll a list until an item is visible, then return it
WebElement item = driver.findElement(AppiumBy.androidUIAutomator(
"new UiScrollable(new UiSelector().scrollable(true))"
+ ".scrollIntoView(new UiSelector().text(\"Laptop Stand Aluminium\"))"));
Gestures
Taps and typing work like Selenium. Swipes, scrolls and long presses use either W3C Actions (PointerInput sequences) or driver-specific mobile commands run through executeScript, which are simpler:
// UiAutomator2: swipe up within a region of the screen
driver.executeScript("mobile: swipeGesture", Map.of(
"left", 100, "top", 500, "width", 600, "height", 1000,
"direction", "up",
"percent", 0.75));
Mobile command names and arguments differ between drivers, so check the driver’s documentation (UiAutomator2 and XCUITest each document their mobile: commands).
Why mobile tests get flaky
Mobile adds variability that web tests rarely see:
- Device and OS fragmentation: screen sizes, OS versions and manufacturer skins change layouts and timing.
- Emulator performance: cold boots, animations and slow CI machines. Disable animations in developer options on test devices and use generous explicit waits.
- System dialogs: permission prompts, keyboards covering fields, battery or update notifications. Grant permissions up front where the driver supports it (UiAutomator2 has
appium:autoGrantPermissions) and hide the keyboard deliberately. - App state: leftover data from earlier tests. Decide on a reset strategy and stick to it.
Running across many real devices is usually done on a device cloud or an in-house device lab rather than on CI agents directly.
Practice exercise
You are automating the checkout of a shopping app on Android and iOS. For each element below, choose a locator strategy and explain why, then write the Android capabilities JSON for a session that installs /builds/shop.apk on the physical device with UDID R58M123ABC, auto-grants permissions and clears app data before the session.
- A “Checkout” button with accessibility id
checkout-buttonon both platforms - A product name in a long, scrollable list, with no id
- A promo code text field with Android resource-id
com.example.shop:id/promoand no iOS identifier - A banner whose only identifying feature is its position in a deeply nested layout
The locator thinking carries over from web: practise it in the Selenium Lab.
Show a model answer
AppiumBy.accessibilityId("checkout-button"): one locator for both platforms, fast and stable.- Android:
androidUIAutomatorwithUiScrollable(...).scrollIntoView(new UiSelector().text("...")), which scrolls and finds in one call. iOS: aniOSNsPredicateStringsuch aslabel == "Laptop Stand Aluminium"after scrolling with amobile: scrollcommand. - Android:
AppiumBy.id("com.example.shop:id/promo"). iOS: ask developers to add an accessibility identifier; until then, an iOS class chain or predicate on the field’s type and placeholder. - Raise it with the developers first. A positional XPath will break on the next layout change and is slow; if you must ship a test now, use it as a clearly marked temporary locator.
Capabilities:
{
"platformName": "Android",
"appium:automationName": "UiAutomator2",
"appium:udid": "R58M123ABC",
"appium:deviceName": "Android Device",
"appium:app": "/builds/shop.apk",
"appium:autoGrantPermissions": true,
"appium:noReset": false,
"appium:fullReset": false
}With noReset: false and fullReset: false, UiAutomator2 clears the app’s data at session start without uninstalling the app, which is faster than a full reset while still isolating tests.
Key takeaways
- Appium is a Node.js server that speaks W3C WebDriver and delegates to drivers: UiAutomator2 for Android, XCUITest for iOS. Install drivers with
appium driver install. - Appium 2 and later serve at the root path (
http://127.0.0.1:4723), and non-standard capabilities need theappium:prefix. - Prefer accessibility ids, then resource ids and platform-native locators; treat XPath as a slow last resort.
- Use Appium Inspector to explore the element tree and test locators.
- Mobile flakiness comes from devices, emulators, system dialogs and app state; design waits, permissions and resets deliberately.
Key terms
Finished this lesson?
Progress is saved in this browser. No account needed.
Practise it
Related interview questions
- What are web-first assertions, and why is expect(await locator.textContent()).toBe() an anti-pattern?
- Explain Browser, BrowserContext and Page in Playwright. Why do contexts matter for test isolation?
- How does Selenium WebDriver work under the hood?
- Which locator strategies do you use in Selenium, and how do you choose?