·Pramod Dutta

Flaky Mobile Tests in Appium: Causes, Patterns, and Reliable Solutions

A comprehensive guide to fixing flaky mobile tests in Appium. Covers element location strategies, implicit vs explicit waits, device farm inconsistencies, app state management, gesture reliability, and proven patterns for stable mobile automation.

appium flaky testsmobile testing flakyappium test failuresmobile automation flakyappium wait strategiesappium element locatormobile test reliabilityappium device farmappium gesture testingmobile test stability

Flaky Mobile Tests in Appium: Causes, Patterns, and Reliable Solutions

Mobile test automation is hard. Appium makes it possible, but it does not make it easy. If you have spent any time writing Appium tests, you know the frustration: a test that passes five times in a row, then fails on the sixth. A gesture that works perfectly on a Pixel but breaks on a Samsung. A login flow that times out in your device farm but never locally.

Appium flaky tests are among the most expensive to debug because mobile environments introduce layers of variability that do not exist in web or API testing. The device hardware, OS version, app rendering speed, network conditions, device farm infrastructure, and even battery state can all influence test outcomes.

This guide breaks down every major source of flakiness in Appium tests and provides concrete, tested solutions for each.

Why Mobile Tests Are Inherently More Flaky

Before diving into solutions, it helps to understand why mobile testing produces more flakiness than other testing domains.

Hardware variability. Even two devices of the same model can behave differently. Screen density, available memory, CPU throttling under heat, and background processes all vary device to device. OS fragmentation. Android alone has thousands of OS-version and manufacturer-skin combinations. Each handles animations, rendering, and system dialogs slightly differently. Network dependency. Mobile apps frequently depend on network calls, and mobile network conditions are inherently variable -- even in CI environments using emulators. Rendering asynchronicity. Mobile UI rendering is asynchronous and influenced by device performance. An element that appears in 200ms on a flagship device might take 2 seconds on a budget phone or an overloaded emulator. Appium architecture. Appium communicates with the device over HTTP using the WebDriver protocol. This adds network latency and introduces timing windows between sending a command and it being executed on the device.

Understanding these factors is essential for writing appium flaky tests solutions that actually stick.

Element Location Strategies

Unreliable element location is the single biggest cause of appium flaky tests. The way you find elements determines whether your test is resilient to layout changes, rendering delays, and platform differences.

The Locator Reliability Hierarchy

From most reliable to least reliable:

  • Accessibility ID -- stable across platforms, semantically meaningful
  • Resource ID / Test ID -- stable within a platform, developer-controlled
  • Class name + index -- fragile, breaks with layout changes
  • XPath -- extremely fragile, slow, and platform-specific
  • Using Accessibility IDs (Best Practice)

    // iOS and Android: Same locator, same test
    

    MobileElement loginButton = driver.findElement(

    AppiumBy.accessibilityId("login-button")

    );

    loginButton.click();

    For this to work, your development team must add accessibility identifiers to UI elements:

    // iOS (SwiftUI)
    

    Button("Log In") {

    viewModel.login()

    }

    .accessibilityIdentifier("login-button")

    // Android (Jetpack Compose)
    

    Button(

    onClick = { viewModel.login() },

    modifier = Modifier.semantics {

    testTag = "login-button"

    }

    ) {

    Text("Log In")

    }

    Why XPath Causes Flakiness

    XPath is the default fallback when no better locator is available, but it is a flakiness magnet:

    // TERRIBLE: Absolute XPath - breaks if ANY element in the tree changes
    

    driver.findElement(By.xpath(

    "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/"

    + "android.widget.FrameLayout/android.widget.FrameLayout/"

    + "android.widget.FrameLayout/android.view.ViewGroup/"

    + "android.widget.Button[2]"

    ));

    // BAD: Relative XPath - still slow and fragile

    driver.findElement(By.xpath(

    "//android.widget.Button[@text='Login']"

    ));

    // GOOD: Accessibility ID

    driver.findElement(AppiumBy.accessibilityId("login-button"));

    XPath queries traverse the entire UI tree, which is slow on mobile devices. The tree structure can change between app versions, OS updates, or even between renders of the same screen. Every XPath locator in your test suite is a ticking time bomb.

    Building a Locator Strategy Guide for Your Team

    Create a decision tree that your team follows:

    Has accessibility ID? → Use accessibilityId()
    

    Has resource-id/testID? → Use id()

    Has unique text? → Use AccessibilityId or predicate (iOS) / UIAutomator (Android)

    None of the above? → Add an accessibility ID to the app code

    Never write an XPath locator. If you cannot find an element without XPath, the correct fix is to add a proper identifier in the app code, not to write a fragile XPath query.

    Implicit vs. Explicit Waits

    Wait strategies in Appium are more nuanced than in web testing because mobile rendering is slower and less predictable.

    The Implicit Wait Trap

    // Setting implicit wait globally
    

    driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));

    Implicit waits apply to every findElement call. This seems convenient but creates two problems:

  • Slow failures: Every assertion on an element that should NOT exist waits the full timeout before failing
  • Interference with explicit waits: When implicit and explicit waits interact, the behavior is unpredictable -- you can end up waiting up to implicit + explicit seconds
  • Explicit Waits: The Right Approach

    // Utility method for reliable element finding
    

    public MobileElement waitForElement(By locator, int timeoutSeconds) {

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds));

    return (MobileElement) wait.until(

    ExpectedConditions.presenceOfElementLocated(locator)

    );

    }

    // Wait for element to be clickable (visible + enabled)

    public MobileElement waitForClickable(By locator, int timeoutSeconds) {

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds));

    return (MobileElement) wait.until(

    ExpectedConditions.elementToBeClickable(locator)

    );

    }

    // Usage in tests

    @Test

    public void testLogin() {

    MobileElement emailField = waitForClickable(

    AppiumBy.accessibilityId("email-input"), 15

    );

    emailField.sendKeys("user@example.com");

    MobileElement passwordField = waitForClickable(

    AppiumBy.accessibilityId("password-input"), 5

    );

    passwordField.sendKeys("password123");

    MobileElement loginBtn = waitForClickable(

    AppiumBy.accessibilityId("login-button"), 5

    );

    loginBtn.click();

    // Wait for the next screen to appear

    waitForElement(AppiumBy.accessibilityId("dashboard-title"), 20);

    }

    Custom Wait Conditions for Mobile

    Standard WebDriver wait conditions do not cover all mobile scenarios. Build custom ones:

    // Wait for a toast message to appear and disappear
    

    public void waitForToast(String expectedText, int timeoutSeconds) {

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds));

    wait.until(driver -> {

    try {

    MobileElement toast = driver.findElement(

    By.xpath("//android.widget.Toast")

    );

    return toast.getText().contains(expectedText);

    } catch (NoSuchElementException e) {

    return false;

    }

    });

    }

    // Wait for loading spinner to disappear

    public void waitForLoadingComplete(int timeoutSeconds) {

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds));

    wait.until(ExpectedConditions.invisibilityOfElementLocated(

    AppiumBy.accessibilityId("loading-spinner")

    ));

    }

    // Wait for list to be populated

    public void waitForListItems(By listLocator, int minItems, int timeoutSeconds) {

    WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds));

    wait.until(driver -> {

    List items = driver.findElements(listLocator);

    return items.size() >= minItems;

    });

    }

    Device Farm Inconsistencies

    Running appium flaky tests on device farms (AWS Device Farm, BrowserStack, Sauce Labs, or self-hosted farms) introduces a new layer of variability. The same test can behave differently across farm sessions due to device state, network routing, and server-side processing.

    Common Device Farm Issues

    1. Device state carryover: Previous test sessions may leave residual state -- cached data, logged-in accounts, changed settings.
    // Always start with a clean app state
    

    DesiredCapabilities caps = new DesiredCapabilities();

    caps.setCapability("noReset", false); // Reset app state

    caps.setCapability("fullReset", false); // Don't reinstall (too slow)

    caps.setCapability(AndroidMobileCapabilityType.AUTO_GRANT_PERMISSIONS, true);

    2. Network latency variation: Device farms add network hops between your test runner and the device.
    // Increase command timeout for device farms
    

    caps.setCapability("newCommandTimeout", 120); // 2 minutes

    caps.setCapability("appWaitDuration", 30000); // 30 seconds for app launch

    3. Device allocation randomness: You might get a fast device one run and a slow device the next.
    // Set generous timeouts that work on the slowest device in your farm
    

    private static final int ELEMENT_TIMEOUT = 20; // seconds

    private static final int PAGE_LOAD_TIMEOUT = 30;

    private static final int APP_LAUNCH_TIMEOUT = 45;

    Normalizing Device Farm Runs

    @BeforeMethod
    

    public void setUp() {

    DesiredCapabilities caps = new DesiredCapabilities();

    // App configuration

    caps.setCapability("app", APP_PATH);

    caps.setCapability("noReset", false);

    caps.setCapability("autoGrantPermissions", true);

    // Performance normalization

    caps.setCapability("disableAnimations", true);

    caps.setCapability("skipDeviceInitialization", true);

    caps.setCapability("skipServerInstallation", true);

    // Stability settings

    caps.setCapability("newCommandTimeout", 180);

    caps.setCapability("appWaitDuration", 30000);

    // For Android: disable unnecessary features

    caps.setCapability("ignoreUnimportantViews", true);

    caps.setCapability("disableWindowAnimation", true);

    driver = new AndroidDriver<>(new URL(FARM_URL), caps);

    driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0));

    }

    App State Management

    Mobile apps have complex state: login status, cached data, push notification permissions, location permissions, onboarding completion flags, and more. Poor state management is a persistent source of mobile testing flaky behavior.

    The Clean State Principle

    Every test should start from a known, clean state. There are three levels of app reset:

    // Level 1: No reset (fastest, but state leaks between tests)
    

    caps.setCapability("noReset", true);

    // Level 2: Fast reset (clears app data, keeps app installed)

    caps.setCapability("noReset", false);

    caps.setCapability("fullReset", false);

    // Level 3: Full reset (uninstalls and reinstalls the app)

    caps.setCapability("fullReset", true); // Slowest, but cleanest

    For most test suites, Level 2 is the right balance. Use Level 3 only when you need to test first-launch experiences.

    Deep Linking to Skip Setup

    Instead of navigating through the app UI to reach the screen under test, use deep links to jump directly there:

    @Test
    

    public void testProductDetails() {

    // BAD: Navigate through UI (slow, many potential failure points)

    // login() -> navigateToShop() -> searchProduct() -> selectProduct()

    // GOOD: Deep link directly to the product page

    driver.get("myapp://product/12345");

    // Now test the actual thing we care about

    MobileElement title = waitForElement(

    AppiumBy.accessibilityId("product-title"), 15

    );

    assertEquals("Widget Pro", title.getText());

    }

    Handling System Dialogs

    System dialogs (permissions, notifications, updates) are a major source of appium flaky tests because they appear unpredictably:

    // Proactive approach: handle dialogs before they disrupt tests
    

    public void dismissSystemDialogs() {

    try {

    // Android: Handle "App not responding" dialog

    WebDriverWait shortWait = new WebDriverWait(driver, Duration.ofSeconds(2));

    MobileElement waitButton = (MobileElement) shortWait.until(

    ExpectedConditions.presenceOfElementLocated(

    By.id("android:id/aerr_wait")

    )

    );

    waitButton.click();

    } catch (TimeoutException ignored) {

    // No dialog present, continue

    }

    // Auto-grant permissions on Android

    try {

    MobileElement allowButton = driver.findElement(

    By.id("com.android.permissioncontroller:id/permission_allow_button")

    );

    allowButton.click();

    } catch (NoSuchElementException ignored) {

    // No permission dialog

    }

    }

    A more robust approach is to handle dialogs at the framework level:

    // Custom event listener that handles dialogs automatically
    

    public class DialogHandler implements WebDriverEventListener {

    @Override

    public void afterFindBy(By by, WebElement element, WebDriver driver) {

    // Check for and dismiss system dialogs after every find operation

    }

    // ... implement other methods

    }

    Gesture Reliability

    Touch gestures (swipe, scroll, pinch, long press) are the most fragile operations in Appium. They depend on precise coordinates, timing, and device-specific behavior.

    Why Gestures Fail

  • Coordinate-based gestures break when screen dimensions or element positions change
  • Timing-sensitive gestures (like long press) behave differently on fast vs. slow devices
  • Multi-touch gestures (pinch, zoom) have platform-specific quirks
  • Scroll gestures can overshoot or undershoot depending on device scroll physics
  • Reliable Scrolling

    Instead of fixed-coordinate swipes, scroll to elements:

    // BAD: Fixed-coordinate swipe (breaks on different screen sizes)
    

    TouchAction action = new TouchAction(driver);

    action.press(PointOption.point(500, 1500))

    .waitAction(WaitOptions.waitOptions(Duration.ofMillis(500)))

    .moveTo(PointOption.point(500, 500))

    .release()

    .perform();

    // GOOD: Scroll until element is visible (Android)

    public void scrollToElement(String accessibilityId) {

    driver.findElement(AppiumBy.androidUIAutomator(

    "new UiScrollable(new UiSelector().scrollable(true))"

    + ".scrollIntoView(new UiSelector()"

    + ".description(\"" + accessibilityId + "\"))"

    ));

    }

    // GOOD: Scroll until element is visible (iOS)

    public void scrollToElement(String accessibilityId) {

    Map params = new HashMap<>();

    params.put("direction", "down");

    params.put("predicateString", "label == '" + accessibilityId + "'");

    driver.executeScript("mobile: scroll", params);

    }

    Reliable Long Press

    // Platform-adaptive long press
    

    public void longPress(MobileElement element, int durationMs) {

    if (driver instanceof AndroidDriver) {

    new Actions(driver)

    .clickAndHold(element)

    .pause(Duration.ofMillis(durationMs))

    .release()

    .perform();

    } else {

    // iOS: Use mobile: touchAndHold

    Map params = new HashMap<>();

    params.put("element", element.getId());

    params.put("duration", durationMs / 1000.0);

    driver.executeScript("mobile: touchAndHold", params);

    }

    }

    Swipe with Retry

    For gestures that are inherently unreliable, build retry logic at the gesture level:

    public void swipeUntilVisible(By targetLocator, String direction, int maxSwipes) {
    

    for (int i = 0; i < maxSwipes; i++) {

    try {

    MobileElement element = driver.findElement(targetLocator);

    if (element.isDisplayed()) {

    return;

    }

    } catch (NoSuchElementException ignored) {

    // Element not found yet, swipe and try again

    }

    performSwipe(direction);

    // Brief pause for rendering

    try { Thread.sleep(500); } catch (InterruptedException ignored) {}

    }

    throw new NoSuchElementException(

    "Element not found after " + maxSwipes + " swipes"

    );

    }

    Building a Resilient Appium Test Framework

    Rather than fixing appium flaky tests one at a time, build reliability into your framework so every test benefits.

    Page Object Pattern with Built-In Waits

    public abstract class BasePage {
    

    protected AppiumDriver driver;

    protected WebDriverWait wait;

    public BasePage(AppiumDriver driver) {

    this.driver = driver;

    this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));

    }

    protected MobileElement find(By locator) {

    return (MobileElement) wait.until(

    ExpectedConditions.presenceOfElementLocated(locator)

    );

    }

    protected void tap(By locator) {

    MobileElement element = (MobileElement) wait.until(

    ExpectedConditions.elementToBeClickable(locator)

    );

    element.click();

    }

    protected void type(By locator, String text) {

    MobileElement element = find(locator);

    element.clear();

    element.sendKeys(text);

    }

    protected boolean isDisplayed(By locator, int timeout) {

    try {

    new WebDriverWait(driver, Duration.ofSeconds(timeout))

    .until(ExpectedConditions.visibilityOfElementLocated(locator));

    return true;

    } catch (TimeoutException e) {

    return false;

    }

    }

    }

    public class LoginPage extends BasePage {

    private final By emailField = AppiumBy.accessibilityId("email-input");

    private final By passwordField = AppiumBy.accessibilityId("password-input");

    private final By loginButton = AppiumBy.accessibilityId("login-button");

    private final By errorMessage = AppiumBy.accessibilityId("error-message");

    public LoginPage(AppiumDriver driver) {

    super(driver);

    }

    public DashboardPage login(String email, String password) {

    type(emailField, email);

    type(passwordField, password);

    tap(loginButton);

    return new DashboardPage(driver);

    }

    public boolean hasError() {

    return isDisplayed(errorMessage, 5);

    }

    }

    Test Stability Checklist

    Before merging any new Appium test, verify:

    • [ ] Uses accessibility IDs exclusively (no XPath)
    • [ ] Uses explicit waits (no implicit waits, no Thread.sleep)
    • [ ] Handles system dialogs
    • [ ] Starts from a clean app state
    • [ ] Works on both the fastest and slowest target devices
    • [ ] Does not depend on network speed
    • [ ] Does not depend on other tests

    Monitoring and Detection

    Even with all these practices, some appium flaky tests will slip through. Continuous monitoring catches them before they become a persistent problem.

    # Run DeFlaky against your Appium test suite
    

    npx deflaky run --framework appium --path ./src/test/java

    Track your mobile test flake rate separately from your web and API tests. Mobile tests will always have a higher baseline flake rate, but that does not mean you should accept it -- it means you should invest proportionally more in mobile test reliability.

    Conclusion

    Appium flaky tests are not a fact of life. They are engineering problems with engineering solutions. The patterns in this guide -- reliable locators, explicit waits, clean state management, gesture resilience, and device farm normalization -- address the root causes of mobile test flakiness rather than masking symptoms with retries.

    Start with the highest-impact change: replace XPath locators with accessibility IDs. This single change will eliminate a significant percentage of your mobile test failures. Then work through the remaining patterns systematically.

    Identify every flaky test in your Appium suite and get prioritized fix recommendations. DeFlaky detects mobile test flakiness patterns and shows you exactly where to focus:
    npx deflaky run
    

    Stop guessing. DeFlaky your tests.

    Detect flaky tests in minutes with a single CLI command.