Skip to content

06 · Mobile Testing with Appium

Appium extends the exact automation model you already know from Selenium WebDriver — find an element, act on it, assert — to native and hybrid mobile apps on Android and iOS. If Level 1's driver.findElement(By.id(...)) is comfortable, Appium's API will feel almost identical; the differences are in locators, drivers, and what "the page" even means on a phone.

1. How Appium fits together

  • Appium Server — a Node.js process implementing the WebDriver protocol (the same protocol RemoteWebDriver used in Module 04's Selenium Grid).
  • DriverUiAutomator2 for Android, XCUITest for iOS; translates WebDriver calls into platform-native automation.
  • Capabilities — a map telling Appium which app, which device/emulator, which platform — the mobile equivalent of ChromeOptions.
  • Your Java test talks to Appium Server exactly like it talks to a Selenium Grid hub: over HTTP, via a RemoteWebDriver subtype.

2. Setup

<dependency>
    <groupId>io.appium</groupId>
    <artifactId>java-client</artifactId>
    <version>9.3.0</version>
    <scope>test</scope>
</dependency>
package com.example.mobile;

import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.options.UiAutomator2Options;
import java.net.URL;

public class AppiumDriverFactory {

    public static AndroidDriver create() throws Exception {
        UiAutomator2Options options = new UiAutomator2Options()
                .setDeviceName("Pixel_7_API_34")
                .setApp("/path/to/app-debug.apk")
                .setAutomationName("UiAutomator2")
                .setNewCommandTimeout(java.time.Duration.ofSeconds(60));

        return new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
    }
}

AndroidDriver implements WebDriver, so everything from Level 1 — explicit waits, findElement, Page Objects from Level 2 — carries over unchanged in structure; only the locator strategy and the driver type change.

3. Mobile locators

import io.appium.java_client.AppiumBy;
import org.openqa.selenium.By;

// Resource id (Android) -- most stable, prefer this first
driver.findElement(AppiumBy.id("com.example.app:id/username_field"));

// Accessibility id -- works cross-platform (Android content-desc / iOS accessibilityIdentifier)
driver.findElement(AppiumBy.accessibilityId("login-button"));

// Android UiAutomator selector -- powerful but verbose and Android-only
driver.findElement(AppiumBy.androidUIAutomator(
        "new UiSelector().text(\"Sign in\")"));

// XPath -- last resort, same trade-offs as in Selenium (Level 1 Module 08)
driver.findElement(By.xpath("//android.widget.Button[@text='Sign in']"));

Prefer, in order: resource-id / accessibility-id, then UiAutomator selector, then XPath — the same "specific over brittle" hierarchy taught for CSS selectors in Level 1, just with mobile-specific tools at the top instead of id/css.

4. A login test

package com.example.mobile;

import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.AppiumBy;
import org.junit.jupiter.api.*;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;

import static org.junit.jupiter.api.Assertions.*;

class MobileLoginTest {

    private AndroidDriver driver;
    private WebDriverWait wait;

    @BeforeEach
    void setUp() throws Exception {
        driver = AppiumDriverFactory.create();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    @AfterEach
    void tearDown() {
        if (driver != null) driver.quit();
    }

    @Test
    void validLoginShowsDashboard() {
        driver.findElement(AppiumBy.accessibilityId("username-field")).sendKeys("alice");
        driver.findElement(AppiumBy.accessibilityId("password-field")).sendKeys("correct-horse");
        driver.findElement(AppiumBy.accessibilityId("login-button")).click();

        wait.until(d -> ((AndroidDriver) d)
                .findElements(AppiumBy.accessibilityId("dashboard-title"))
                .size() > 0);

        assertTrue(driver.findElement(AppiumBy.accessibilityId("dashboard-title")).isDisplayed());
    }
}

The explicit wait matters even more on mobile than web: app transitions involve animations and sometimes a fresh Activity/ViewController load, both slower and less predictable than a web page's DOM update.

5. Gestures

import io.appium.java_client.android.nativekey.AndroidKey;
import io.appium.java_client.android.nativekey.KeyEvent;
import org.openqa.selenium.interactions.PointerInput;
import org.openqa.selenium.interactions.Sequence;
import java.util.List;
import java.time.Duration;

// Native back button
driver.pressKey(new KeyEvent(AndroidKey.BACK));

// Swipe up (e.g. to scroll a list) using W3C Actions
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence swipeUp = new Sequence(finger, 0)
    .addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), 500, 1500))
    .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
    .addAction(finger.createPointerMove(Duration.ofMillis(300), PointerInput.Origin.viewport(), 500, 300))
    .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
driver.perform(List.of(swipeUp));

6. iOS differs mainly in the options object and locators

import io.appium.java_client.ios.IOSDriver;
import io.appium.java_client.ios.options.XCUITestOptions;

XCUITestOptions options = new XCUITestOptions()
        .setDeviceName("iPhone 15")
        .setApp("/path/to/App.app")
        .setAutomationName("XCUITest");

IOSDriver iosDriver = new IOSDriver(new URL("http://127.0.0.1:4723"), options);
iosDriver.findElement(AppiumBy.accessibilityId("login-button"));  // accessibilityId works on both platforms

Writing tests against AppiumBy.accessibilityId(...) wherever the app exposes one (rather than platform-specific id/UIAutomator selectors) is what lets the same test class run against both platforms with only the driver factory swapped — mirroring the cross-browser pattern from Module 04.

I did not run any of this in this environment — no Android SDK, emulator, Appium Server, or iOS toolchain available headlessly here. This entire module is reviewed against Appium's java-client 9.x documented API and mirrors the WebDriver patterns already verified working (structurally) in Level 1/2's Selenium modules; treat it as reviewed, not executed.

7. Testing traps

Trap 1 — resource-ids that change per build

Some Android build tools obfuscate/randomize resource ids across release builds. A locator hard-coded against a debug-build id silently breaks against the release APK. Prefer accessibilityId/content-desc, which app developers set deliberately and should keep stable.

Trap 2 — emulator cold-start flakiness

The first test after an emulator boots is often slower and less reliable than the tenth — animations haven't settled, background services are still starting. A "flaky" first test that passes on retry is frequently this, not a real defect; warm the emulator with a no-op launch before the real suite runs.

Trap 3 — session not properly torn down

Skipping driver.quit() (e.g. a test throwing before @AfterEach runs due to a setup exception) leaves the app session open, and the next test's create() call either hangs waiting for a device slot or grabs a dirty app state. Always tear down in @AfterEach, and consider a finally around risky setup.

Trap 4 — testing on one device profile only

A layout assumption that holds on a Pixel 7 emulator (tall aspect ratio, specific density) can break on a smaller or older device where the same element is off-screen and needs a scroll first. Run the real suite across at least a small/medium/large device matrix before trusting it.

Trap 5 — permission dialogs blocking the flow

A location/camera/notification permission dialog appearing mid-test (OS popups aren't part of your app's element tree) causes every subsequent findElement to time out looking for something the dialog is covering. Grant permissions via capabilities (autoGrantPermissions: true on Android) or handle the dialog explicitly rather than hoping it never appears.

Cheat sheet

Task Code
Android capabilities UiAutomator2Options().setDeviceName(...).setApp(...)
iOS capabilities XCUITestOptions().setDeviceName(...).setApp(...)
Connect to Appium Server new AndroidDriver(new URL("http://127.0.0.1:4723"), options)
Resource id locator (Android) AppiumBy.id("pkg:id/name")
Cross-platform locator AppiumBy.accessibilityId("name")
UiAutomator selector AppiumBy.androidUIAutomator("new UiSelector()...")
Native back driver.pressKey(new KeyEvent(AndroidKey.BACK))
Swipe/gesture W3C Sequence + PointerInput
Auto-grant permissions autoGrantPermissions: true capability
Always tear down driver.quit() in @AfterEach

How It Actually Works

Appium's UiAutomator2 driver on Android reveals what "the same WebDriver protocol" actually spans: your Java client sends the identical POST /session/{id}/element JSON request as any Selenium test, but the Appium Server (a Node process) translates that request into commands for a small instrumentation APK it installs and runs on the device/emulator alongside your app under test — Google's UiAutomator2 framework. That instrumentation process runs inside the same Android system process space as accessibility services, which is how it can query the entire live view hierarchy (every View/Activity/Fragment currently rendered) and translate a locator like AppiumBy.androidUIAutomator("new UiSelector().text(\"Login\")") into a real query against that hierarchy, then simulate the actual touch event at the resolved coordinates using Android's own input-injection APIs — so a "tap" in Appium is mechanically a synthetic MotionEvent dispatched through the same OS input pipeline a real finger touch would generate, not a JavaScript click on a DOM node the way Selenium's is.

This is exactly why "the page" means something different on mobile: there is no DOM, so getPageSource() in Appium returns a serialized dump of the live native view hierarchy (XML resembling Android's layout tree) rather than HTML, and locators target that tree's resource-id/class/text attributes instead of CSS selectors. iOS's XCUITest driver follows the same pattern one layer differently: it embeds Apple's own XCTest framework (normally used for native unit/UI tests) as a bridge process, translating WebDriver calls into XCUIElement queries against iOS's accessibility tree — a different underlying automation technology per platform, unified only by the shared WebDriver wire protocol your Java code speaks.

Exercise

  1. Install Appium Server and an Android emulator (or document that you're reviewing only), write AppiumDriverFactory, and confirm a session opens against a sample APK.
  2. Write MobileLoginTest against a real or sample app, using accessibilityId locators throughout.
  3. Add a swipe-to-scroll gesture to reach an element below the fold, and assert it becomes visible/clickable after the swipe.
  4. Deliberately skip granting a permission your app requests on launch, run the suite, and capture the exact timeout/error findElement produces — this is Trap 5 made concrete.
  5. Design (in comments, no device required if you don't have one) a small device matrix — at minimum a small-screen and large-screen Android profile — and list which of your existing locators you'd expect to break on the small-screen profile and why.