← All skills

Appium Skill

Hot
Mobile testingJavaPythonJavaScriptRubyC#

Copy and Paste in your Terminal

npx skills add https://github.com/LambdaTest/agent-skills.git --skill appium-skill

Playbook

Complete implementation guide with code samples, patterns, and best practices.

Appium — Advanced Implementation Playbook

§1 — Project Setup & Capabilities

<!-- pom.xml -->
<dependencies>
    <dependency>
        <groupId>io.appium</groupId>
        <artifactId>java-client</artifactId>
        <version>9.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.18.1</version>
    </dependency>
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>7.9.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>io.qameta.allure</groupId>
        <artifactId>allure-testng</artifactId>
        <version>2.25.0</version>
    </dependency>
</dependencies>

Android Capabilities

UiAutomator2Options androidOptions() {
    return new UiAutomator2Options()
        .setDeviceName(System.getProperty("device.name", "Pixel_6"))
        .setPlatformVersion(System.getProperty("platform.version", "14"))
        .setApp(System.getProperty("app.path", "src/test/resources/app-debug.apk"))
        .setAutomationName("UiAutomator2")
        .setAutoGrantPermissions(true)
        .setNoReset(Boolean.parseBoolean(System.getProperty("no.reset", "false")))
        .setNewCommandTimeout(Duration.ofSeconds(300))
        .setAdbExecTimeout(Duration.ofSeconds(60))
        .setUiautomator2ServerInstallTimeout(Duration.ofSeconds(120))
        .setAmplitude(new HashMap<>() {{ put("appWaitActivity", "*"); }});
}

iOS Capabilities

XCUITestOptions iosOptions() {
    return new XCUITestOptions()
        .setDeviceName(System.getProperty("device.name", "iPhone 15"))
        .setPlatformVersion(System.getProperty("platform.version", "17.2"))
        .setApp(System.getProperty("app.path", "src/test/resources/MyApp.app"))
        .setAutomationName("XCUITest")
        .setWdaLaunchTimeout(Duration.ofSeconds(120))
        .setWdaConnectionTimeout(Duration.ofSeconds(120))
        .setAutoAcceptAlerts(true)
        .setShowXcodeLog(true);
}

§2 — BaseTest with Thread-Safe Driver

public class BaseTest {
    protected static final ThreadLocal<AppiumDriver> driverThread = new ThreadLocal<>();
    protected AppiumDriver driver;

    @Parameters({"platform", "deviceName", "platformVersion"})
    @BeforeMethod(alwaysRun = true)
    public void setUp(@Optional("android") String platform,
                      @Optional("Pixel_6") String deviceName,
                      @Optional("14") String platformVersion) throws Exception {
        String appiumUrl = System.getProperty("appium.url", "http://127.0.0.1:4723");

        BaseOptions<?> options;
        if ("ios".equalsIgnoreCase(platform)) {
            options = iosOptions()
                .setDeviceName(deviceName)
                .setPlatformVersion(platformVersion);
        } else {
            options = androidOptions()
                .setDeviceName(deviceName)
                .setPlatformVersion(platformVersion);
        }

        AppiumDriver d = new AppiumDriver(new URL(appiumUrl), options);
        d.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
        driverThread.set(d);
        driver = d;
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown(ITestResult result) {
        AppiumDriver d = driverThread.get();
        if (d != null) {
            if (result.getStatus() == ITestResult.FAILURE) {
                captureScreenshot(result.getName());
            }
            d.quit();
            driverThread.remove();
        }
    }

    public AppiumDriver getDriver() { return driverThread.get(); }

    private void captureScreenshot(String testName) {
        try {
            byte[] screenshot = driverThread.get().getScreenshotAs(OutputType.BYTES);
            Allure.addAttachment(testName + "-failure", "image/png",
                new ByteArrayInputStream(screenshot), "png");
        } catch (Exception e) { /* log */ }
    }

    protected boolean isAndroid() {
        return driver.getCapabilities().getPlatformName()
            .toString().equalsIgnoreCase("android");
    }
}

§3 — Cross-Platform Page Objects

public class LoginScreen {
    private final AppiumDriver driver;
    private final WebDriverWait wait;

    @AndroidFindBy(accessibility = "email_input")
    @iOSXCUITFindBy(accessibility = "email_input")
    private WebElement emailField;

    @AndroidFindBy(accessibility = "password_input")
    @iOSXCUITFindBy(accessibility = "password_input")
    private WebElement passwordField;

    @AndroidFindBy(accessibility = "login_button")
    @iOSXCUITFindBy(accessibility = "login_button")
    private WebElement loginBtn;

    @AndroidFindBy(id = "com.app:id/error_text")
    @iOSXCUITFindBy(iOSNsPredicate = "type == 'XCUIElementTypeStaticText' AND name CONTAINS 'error'")
    private WebElement errorMsg;

    public LoginScreen(AppiumDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));
        PageFactory.initElements(new AppiumFieldDecorator(driver, Duration.ofSeconds(15)), this);
    }

    public HomeScreen login(String email, String password) {
        emailField.clear(); emailField.sendKeys(email);
        passwordField.clear(); passwordField.sendKeys(password);
        hideKeyboard();
        loginBtn.click();
        return new HomeScreen(driver);
    }

    public String getError() {
        wait.until(ExpectedConditions.visibilityOf(errorMsg));
        return errorMsg.getText();
    }

    public boolean isDisplayed() {
        try { return emailField.isDisplayed(); }
        catch (Exception e) { return false; }
    }

    private void hideKeyboard() {
        try { driver.hideKeyboard(); } catch (Exception e) { /* not shown */ }
    }
}

§4 — Advanced Gestures (W3C Actions)

public class GestureHelper {
    private final AppiumDriver driver;

    public GestureHelper(AppiumDriver driver) { this.driver = driver; }

    // Swipe direction
    public void swipe(Direction direction, double percent) {
        Dimension size = driver.manage().window().getSize();
        int startX, startY, endX, endY;
        switch (direction) {
            case LEFT:
                startX = (int)(size.width * 0.8); endX = (int)(size.width * 0.2);
                startY = endY = size.height / 2; break;
            case RIGHT:
                startX = (int)(size.width * 0.2); endX = (int)(size.width * 0.8);
                startY = endY = size.height / 2; break;
            case UP:
                startY = (int)(size.height * 0.8); endY = (int)(size.height * 0.2);
                startX = endX = size.width / 2; break;
            case DOWN:
                startY = (int)(size.height * 0.2); endY = (int)(size.height * 0.8);
                startX = endX = size.width / 2; break;
            default: throw new IllegalArgumentException("Invalid direction");
        }
        performSwipe(startX, startY, endX, endY, 600);
    }

    private void performSwipe(int startX, int startY, int endX, int endY, int durationMs) {
        PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
        Sequence swipe = new Sequence(finger, 0)
            .addAction(finger.createPointerMove(Duration.ZERO,
                PointerInput.Origin.viewport(), startX, startY))
            .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
            .addAction(finger.createPointerMove(Duration.ofMillis(durationMs),
                PointerInput.Origin.viewport(), endX, endY))
            .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
        driver.perform(Collections.singletonList(swipe));
    }

    // Long press
    public void longPress(WebElement element, int durationMs) {
        Point center = element.getLocation();
        Dimension elemSize = element.getSize();
        int x = center.x + elemSize.width / 2;
        int y = center.y + elemSize.height / 2;

        PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
        Sequence longPress = new Sequence(finger, 0)
            .addAction(finger.createPointerMove(Duration.ZERO,
                PointerInput.Origin.viewport(), x, y))
            .addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
            .addAction(new Pause(finger, Duration.ofMillis(durationMs)))
            .addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
        driver.perform(Collections.singletonList(longPress));
    }

    // Pinch zoom
    public void pinchZoom(int centerX, int centerY, int distance) {
        PointerInput f1 = new PointerInput(PointerInput.Kind.TOUCH, "finger1");
        PointerInput f2 = new PointerInput(PointerInput.Kind.TOUCH, "finger2");

        Sequence s1 = new Sequence(f1, 0)
            .addAction(f1.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), centerX, centerY))
            .addAction(f1.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
            .addAction(f1.createPointerMove(Duration.ofMillis(600),
                PointerInput.Origin.viewport(), centerX, centerY - distance))
            .addAction(f1.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));

        Sequence s2 = new Sequence(f2, 0)
            .addAction(f2.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), centerX, centerY))
            .addAction(f2.createPointerDown(PointerInput.MouseButton.LEFT.asArg()))
            .addAction(f2.createPointerMove(Duration.ofMillis(600),
                PointerInput.Origin.viewport(), centerX, centerY + distance))
            .addAction(f2.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));

        driver.perform(Arrays.asList(s1, s2));
    }

    // Scroll to element — Android
    public WebElement scrollToText(String text) {
        return driver.findElement(AppiumBy.androidUIAutomator(
            "new UiScrollable(new UiSelector().scrollable(true))" +
            ".scrollIntoView(new UiSelector().text(\"" + text + "\"))"));
    }

    // Scroll to element — iOS
    public void scrollToElementIOS(String label) {
        driver.executeScript("mobile: scroll", Map.of(
            "direction", "down",
            "predicateString", "label == '" + label + "'"
        ));
    }

    enum Direction { UP, DOWN, LEFT, RIGHT }
}

§5 — WebView & Hybrid App Testing

// Switch between NATIVE and WEBVIEW contexts
@Test
public void testHybridApp() {
    // Start in native context
    loginScreen.login("user@test.com", "pass123");

    // Wait for WebView
    new WebDriverWait(driver, Duration.ofSeconds(20)).until(d -> {
        Set<String> contexts = ((SupportsContextSwitching) d).getContextHandles();
        return contexts.stream().anyMatch(c -> c.contains("WEBVIEW"));
    });

    // Switch to WebView
    Set<String> contexts = ((SupportsContextSwitching) driver).getContextHandles();
    String webViewContext = contexts.stream()
        .filter(c -> c.contains("WEBVIEW"))
        .findFirst().orElseThrow();
    ((SupportsContextSwitching) driver).context(webViewContext);

    // Now use web selectors
    driver.findElement(By.cssSelector("[data-testid='web-content']")).click();

    // Switch back to native
    ((SupportsContextSwitching) driver).context("NATIVE_APP");
    assertTrue(driver.findElement(AppiumBy.accessibilityId("home_tab")).isDisplayed());
}

§6 — Device Interactions

// Push/pull files
driver.pushFile("/sdcard/Download/test.txt", new File("testdata/test.txt"));
byte[] fileData = driver.pullFile("/sdcard/Download/result.txt");

// App lifecycle
driver.terminateApp("com.myapp");
driver.activateApp("com.myapp");
boolean isRunning = driver.isAppInstalled("com.myapp");
driver.installApp("/path/to/app.apk");
driver.removeApp("com.myapp");

// Notifications (Android)
driver.openNotifications();
driver.findElement(AppiumBy.xpath("//android.widget.TextView[@text='New message']")).click();

// Clipboard
driver.setClipboardText("test data");
String clipboardText = driver.getClipboardText();

// Device orientation
driver.rotate(ScreenOrientation.LANDSCAPE);
driver.rotate(ScreenOrientation.PORTRAIT);

// Geolocation
driver.setLocation(new Location(37.7749, -122.4194, 10));  // San Francisco

// Network conditions (Android)
driver.toggleWifi();
driver.toggleAirplaneMode();
driver.toggleData();

// Deep linking
driver.get("myapp://screen/settings");

§7 — Parallel Device Execution

<!-- testng.xml — multi-device parallel -->
<suite name="Mobile Suite" parallel="tests" thread-count="4">
    <listeners>
        <listener class-name="listeners.AllureListener"/>
        <listener class-name="listeners.RetryListener"/>
    </listeners>

    <test name="Android Pixel 6">
        <parameter name="platform" value="android"/>
        <parameter name="deviceName" value="Pixel_6"/>
        <parameter name="platformVersion" value="14"/>
        <parameter name="appiumPort" value="4723"/>
        <classes><class name="tests.LoginTest"/></classes>
    </test>

    <test name="Android Samsung S23">
        <parameter name="platform" value="android"/>
        <parameter name="deviceName" value="Samsung_S23"/>
        <parameter name="platformVersion" value="14"/>
        <parameter name="appiumPort" value="4724"/>
        <classes><class name="tests.LoginTest"/></classes>
    </test>

    <test name="iOS iPhone 15">
        <parameter name="platform" value="ios"/>
        <parameter name="deviceName" value="iPhone 15"/>
        <parameter name="platformVersion" value="17.2"/>
        <parameter name="appiumPort" value="4725"/>
        <classes><class name="tests.LoginTest"/></classes>
    </test>
</suite>

§8 — LambdaTest Real Device Cloud

public AppiumDriver createLambdaTestDriver(String platform, String device) throws Exception {
    String username = System.getenv("LT_USERNAME");
    String accessKey = System.getenv("LT_ACCESS_KEY");
    String url = "https://" + username + ":" + accessKey + "@mobile-hub.lambdatest.com/wd/hub";

    HashMap<String, Object> ltOptions = new HashMap<>();
    ltOptions.put("build", "Build " + System.getenv("BUILD_NUMBER"));
    ltOptions.put("name", "Appium Tests");
    ltOptions.put("isRealMobile", true);
    ltOptions.put("video", true);
    ltOptions.put("network", true);
    ltOptions.put("console", true);
    ltOptions.put("visual", true);

    if ("android".equalsIgnoreCase(platform)) {
        UiAutomator2Options options = new UiAutomator2Options()
            .setDeviceName(device)
            .setPlatformVersion("14")
            .setApp("lt://APP_ID")
            .setCapability("LT:Options", ltOptions);
        return new AppiumDriver(new URL(url), options);
    } else {
        XCUITestOptions options = new XCUITestOptions()
            .setDeviceName(device)
            .setPlatformVersion("17")
            .setApp("lt://APP_ID")
            .setCapability("LT:Options", ltOptions);
        return new AppiumDriver(new URL(url), options);
    }
}

§9 — CI/CD Integration

# GitHub Actions
name: Appium Tests
on: [push, pull_request]
jobs:
  android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 17 }
      - name: Start emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          target: google_apis
          arch: x86_64
          script: |
            npm install -g appium
            appium driver install uiautomator2
            appium &
            sleep 10
            mvn test -DsuiteXmlFile=testng-android.xml -Dplatform=android
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: appium-results
          path: |
            target/surefire-reports/
            allure-results/
            screenshots/

  cloud:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: temurin, java-version: 17 }
      - run: mvn test -DsuiteXmlFile=testng-cloud.xml
        env:
          LT_USERNAME: ${{ secrets.LT_USERNAME }}
          LT_ACCESS_KEY: ${{ secrets.LT_ACCESS_KEY }}

§10 — Debugging Quick-Reference

ProblemCauseFix
Element not foundWrong locator strategyUse Appium Inspector; prefer accessibilityId
Session creation failsIncompatible capabilitiesVerify device name, platform version, app path
App not installedInvalid APK/IPA pathUse absolute path, verify file exists
iOS WDA build failsXcode/provisioning issueClean DerivedData, increase wdaLaunchTimeout
Android permission dialogRuntime permission blockingSet autoGrantPermissions: true
Slow element findingXPath locatorSwitch to accessibilityId or id (10x faster)
Keyboard covers elementSoft keyboard blockingCall driver.hideKeyboard() before tap
Touch action failsWrong coordinatesUse element center: element.getRect()
WebView not foundContext not switchedList contexts first, wait for WEBVIEW context
App crash mid-testApp state corruptedUse noReset: false for clean state between tests
Flaky on real devicesTiming/animation issuesIncrease waits, disable animations in developer options
Parallel port conflictsSame Appium portUse different systemPort per device

§11 — Best Practices Checklist

  • ✅ Use accessibilityId locator for cross-platform (fastest, most stable)
  • ✅ Use @AndroidFindBy / @iOSXCUITFindBy for platform-specific elements
  • ✅ Use ThreadLocal<AppiumDriver> for parallel-safe execution
  • ✅ Use W3C Actions API for gestures (not deprecated TouchAction)
  • ✅ Use GestureHelper utility class for reusable swipe/scroll/zoom
  • ✅ Set autoGrantPermissions: true (Android) and autoAcceptAlerts: true (iOS)
  • ✅ Use noReset: true for speed, fullReset: true for clean state
  • ✅ Call hideKeyboard() after text input before next interaction
  • ✅ Use Appium Inspector for locator discovery and validation
  • ✅ Capture screenshots + page source on failure via listener
  • ✅ Run on real device cloud (LambdaTest) for production confidence
  • ✅ Use WebView context switching for hybrid app testing
  • ✅ Structure: screens/, tests/, utils/, testdata/, listeners/