Oct 3, 2026
Flaky UI Tests: Why Mobile Tests Break and How to Fix Them
A single flaky test can waste 30 minutes of CI time every day. Fifty flaky tests can stall a pipeline for hours, and a team that stops trusting the results stops reading them. This guide covers the five causes of flaky UI tests on mobile, fixes that work, and an approach that removes whole flakiness classes.
What Makes a UI Test Flaky?
A flaky test passes and fails on the same code without any change in the app. The result depends on timing, environment, or state instead of the feature you test. One flaky test costs you re-runs. Ten cost you trust, and a suite nobody trusts is a suite nobody reads.
The scale is not small: Google reported that almost 16% of its own tests showed some level of flakiness.
Cause 1: Selectors Drift Under Your Tests
Most mobile test frameworks find elements by selectors: accessibility IDs, labels, view hierarchy paths. Developers change these things all the time. A designer renames a button label, or a developer wraps a view in a new container. The test fails because its pointer to the element is gone.
This is the top source of flaky XCUITest tests. A query that matched one element yesterday matches two today, or none. The failure looks random because nothing in your test code moved.
Fix: prefer stable accessibility IDs over visible text, and treat test IDs as a contract with developers. The contract still needs maintenance after every redesign.
Cause 2: Timing and Animation Waits
Mobile UIs animate all the time: screen transitions, keyboard appearance, list refreshes, skeleton loaders. A tap that lands during an animation can miss the element or hit the wrong one.
The classic fix is a fixed sleep. Sleeps slow the suite down and still fail on a loaded CI machine. Explicit waits work better, but you write them for every transition, and each wait is code that can break.
Fix: wait for a visible outcome, not for a duration. "Wait until the Save button appears" beats "wait 2 seconds."
Cause 3: Async Data Arrives Late
Your screen often depends on a network response. The list renders empty, fills a second later, then re-sorts. A test that checks the list too early fails, and it fails only when the network or the simulator is slow that day.
Backend state adds more noise. Another test can mutate your test user's data, and the failure looks like a timing issue.
Fix: seed the data your test needs, and assert on the records it created. Stub the network where the integration is not the point of the test.
Cause 4: Test Order Dependence
A test passes alone and fails in the suite. The cause is shared state: a previous test left the app logged in, filled a form, or created records your test did not expect. Retries hide this: the re-run starts from a different state, the test goes green, and the problem stays.
Fix: isolate tests. Reset the app state before each test, give each test its own data, and keep tests independent from each other.
Cause 5: Your Machine Is Not the CI Machine
"Works on my Mac" is an environment bug. CI runners differ in CPU speed, simulator versions, locale, and timezone. A 300 ms animation can take a second on a busy runner. Tests that pass locally and fail on CI point to this class.
Fix: pin the environment. Fix the simulator version, locale, and timezone, and turn off animations where the platform allows it. Treat a CI-only failure as an environment bug to reproduce, not a retry candidate.
Retries Hide the Problem
Retries help during diagnosis: turn them on, collect traces and screenshots, find the cause. As a permanent setting, they turn red into green while the instability stays. A suite that "eventually passes" trains the team to ignore failures, and a real bug can hide in the noise for weeks.
Improving UI test reliability means fixing causes, not hiding them. A suite trusted at zero retries is the goal.
The Coordinates and Snapshot Approach
Every fix above works, and every one is ongoing work. Selector contracts, wait discipline, data isolation, environment pinning: someone on your team owns this maintenance forever.
Qampanion takes a different route. It is a native macOS app for regression and e2e UI testing of iOS and Android apps. You build a test from visual steps: tap, swipe, type, wait. The steps use coordinates instead of selectors, so a renamed accessibility ID or a changed view hierarchy cannot break your test. The selector class of flakiness stops existing.
Verification works through screenshot comparison. Each step compares the actual screen with an expected snapshot, and a configurable threshold controls how strict the match is. Dynamic areas like clocks, counters, and ads go into excluded zones, so unstable pixels do not fail the step. This removes most of the timing and async-data flakiness. The test checks what the user sees on a settled screen, not an element's internal state mid-animation.
The environment cause shrinks too. Qampanion runs tests on iOS simulators and Android emulators on your own Mac, and the CLI runs the same project on a Mac CI runner. Your machine and CI run the same setup, so "works on my Mac" stops being a mystery. The built-in MCP server lets an AI agent generate the test once. Every replay after that is deterministic and costs zero tokens per run.
Qampanion gives free access, including commercial use. Download the app, and the documentation walks you through snapshots and excluded zones. Our guide on building e2e regression tests without code shows the full workflow from first test to CI.
FAQ
Why are my UI tests flaky?
Most flaky UI tests come from five causes: drifting selectors, animation timing, late network data, shared state, and environment differences between machines. Find your failure's class before you add retries.
How do I fix flaky mobile tests?
Match the fix to the cause. Combine stable accessibility IDs, waits on visible outcomes, seeded data, state resets, and a pinned simulator and locale. Then measure mobile test stability with retries off.
Are flaky XCUITest tests different from flaky Android tests?
The causes are the same on both platforms: element queries, timing, async data, shared state, and environment. The tools differ, but the fixes transfer. If your suite runs on iOS and Android, fix the shared causes first.
How many retries are acceptable in a UI test suite?
Zero as a permanent setting. Use retries during diagnosis to collect traces of the failure, then fix the cause and turn retries off for that test. A retry count above zero means the suite reports instability as success.
Conclusion
Flaky tests have causes, and each cause has a fix: stable selectors, outcome-based waits, isolated data, reset state, and a pinned environment. Apply these fixes and your suite earns back trust.
If you want to skip the maintenance behind those fixes, a coordinate-based tool removes the selector and timing classes of flakiness by design. Download Qampanion, replay one of your flaky flows in it, and compare the stability yourself.