Flaky tests are the most expensive kind of failure in mobile automation. A hard failure points at a bug; a flaky one points at nothing, and every false red build it causes teaches your team to ignore red builds. When I built our mobile automation workspace with Maestro, my first suites flaked constantly — and almost none of it was random. Mobile UI tests flake for a short list of predictable reasons, and each reason has a concrete fix.
Where flaky tests actually come from
Before you fix anything, name the cause. Nearly every flaky mobile test I have debugged traces back to one of five things:
- Animations. The element exists, but it is mid-transition, so the tap lands on nothing.
- Network variability. A screen that renders in one second on office Wi-Fi takes four on a throttled CI runner.
- Emulator cold starts. The first test of a CI run competes with the operating system for resources and times out.
- Time-dependent data. Assertions on today's date, or fixtures that were valid last sprint and have quietly expired since.
- Bad selectors. Matching on display text that changes with copy edits, localization, or RTL layout.
Wait for conditions, not durations
Fixed sleeps are the classic false fix: too short and the test still flakes, too long and the suite crawls. The discipline that actually works is to wait for an observable app state before every action. In Maestro, that means leaning on assertions and extended waits instead of timers:
appId: com.example.shop
---
- launchApp:
clearState: true
- extendedWaitUntil:
visible:
id: "home_feed_list"
timeout: 10000
- tapOn:
id: "checkout_button"
- assertVisible:
id: "order_total"
Every step verifies a real state before acting on it. If the feed never loads, the failure says exactly which condition timed out — that is debugging information, not noise.
Stable selectors and clean state
Ask developers to add test IDs to every element automation touches. It feels like asking for a favor the first time; after a sprint it becomes a habit enforced in code review. IDs survive copy changes, localization, and RTL mirroring — something I care about personally, because I test Arabic builds, where text-based selectors break twice as often.
State reset matters just as much. Launch with cleared state, seed the data each test needs, and never let one test depend on another test's leftovers. A test that only passes when it runs second is already broken; it just has not failed yet.
Retries and quarantine, used honestly
A retry policy is fine; a silent one is not. Retrying a failed test once is reasonable on mobile, but the report must show that it passed on retry, and anything that needs retries repeatedly should be investigated rather than tolerated. Otherwise retries become a machine for hiding real race conditions — some of the best bugs I have found started as "flaky" tests that were flaking for a reason.
For persistent offenders, use a quarantine lane: the test still runs on every build, but its result no longer blocks the pipeline. Every quarantined test gets a JIRA ticket and an owner, so quarantine stays a waiting room, not a graveyard.
Takeaway: treat every flaky test as either a test bug or an app bug — never as weather. Find which of the five causes is at work, fix that, and only then consider retries.
This is unglamorous work, but it is what made our suites trustworthy enough to gate releases. Stability came first; the speed gains from automation only counted once people believed the results.