Why Your UI Tests Flake, and the Fix for Web and Mobile
Flaky UI tests teach a team to ignore red. Fix the three usual causes: brittle locators, fixed sleeps and shared data. Use role and test-id locators, web-first assertions, API setup, Accessibility IDs on mobile, and same-day quarantine.
Key takeaways
- If a check can be proven over HTTP, it belongs in the API suite, not in a browser test.
- Prefer test ids and role-based locators; avoid absolute XPath, generated CSS classes and index-based selection.
- Replace fixed sleeps with assertions that wait for a condition, such as Playwright's toHaveText.
- Create each test's data through the API so no two tests share mutable records.
- On mobile, one Accessibility ID shared by the iOS and Android builds lets one locator find the element on both.
- Quarantine a flaky test the day it flakes, with an owner, instead of retrying the whole suite.
Contents
FAQ
What causes flaky UI tests?
Most often brittle locators that break on a redesign, fixed waits that are too short on a slow run, and tests that share data with each other. Each has a direct fix: stable locators, waits on conditions and data created per test.
Why is XPath slow in Appium?
Mobile operating systems don't query XML natively, so Appium has to serialise the whole native view into an XML string before it can run the XPath. It is slow, and it breaks when the layout nesting changes.
Is retrying failed tests a good fix for flakiness?
Only as a short, logged measure while the cause is fixed. Blanket retries hide real race conditions in the product. Quarantine the flaky test, give it an owner, fix it and restore it.
Sources
Only you and the owner see your answer and rating. Send private feedback
Enjoyed it? Support fx-blog with a coffee.
Related posts
AI in Testing: Use It Without Trusting It, and Test AI Features
Trusting AI output gives you tests that can't fail and an assistant that invents policy. Review every AI-written test by breaking the feature on purpose, keep agents out of the gate, and test AI features with eval sets run in CI.
Quality Gates in CI: Make Your Tests Block a Bad Merge
Tests that run only when someone remembers put the gate in a person. Run them in four CI tiers that each block the next, keep the commit gate under ten minutes, give every run fresh data, and quarantine flaky tests the same day.
Green Tests, Broken Checkout: The Checks Functional Tests Miss
A green functional suite can't see a renamed field, a slow peak, an open security hole or a form a keyboard can't finish. Add a Pact contract, a k6 test against an SLO, a ZAP baseline scan, and axe plus a keyboard pass.
Comments
Oldest firstNo comments yet. Start the conversation.
Part 3 covered the API, and it was never the problem: BUG-311 lives in the page, which shows €27.00 after the API has removed the code. So the next test has to wait for that total in a browser, without
sleep(5), and then run the same checkout on two phones.The repo records an AI assistant writing this:
Three seconds is too long on a fast day and too short on a slow one. The selectors break on the next redesign.
The QA Skills Path, in seven parts:
Locators that survive a redesign
First, keep the browser suite small. It is the slowest, most fragile layer, and the repo's rule is that a check you can prove over HTTP does not belong in a browser test. The ShopLite checkout flow (add to cart, apply code, pay) belongs here. The boundary and decision-table cases from part 2 belong in part 3's API suite.
For the tests that stay, use the highest locator on this list that the page offers:
page.getByTestId('order-total'). Ask developers to add them.page.getByRole('button', { name: 'Apply' }).id,nameoraria-label, if it isn't generated..css-1x2y3z, ornth(4).Arrange through the API, wait in the browser
This test from the repo's test-data guide fixes both other causes at once. It creates its own discount code through the API, so it shares nothing with any other test. Then it clicks through the page and waits for the total, not for a clock:
toHaveTextretries until the text matches or time runs out, so it passes as soon as the page is ready. For BUG-311, the same test then sets the quantity to 1 and expects the bug report's expected total. Today it fails, for the right reason.The same rules on a phone
Mobile has its own locator order, and the top of it works on both platforms: an Accessibility ID. Android maps it to
content-desc, iOS toaccessibilityIdentifier. If both teams use the same string, one line finds the button on both phones:XPath is the trap: Appium first serialises the whole native view into an XML string, which is slow, and the path breaks when a developer wraps the button in a new container. The repo's parallel example runs the suite on two emulators, each with its own Appium server:
When a test flakes anyway
The repo's policy, in order:
npx playwright show-trace).Blanket auto-retry is not on the list. Some flakes are real race conditions in the product.
Try it
The starter is TypeScript; Playwright also runs from Python, which is how part 6's pipeline runs it.
The ShopLite checkout is now green on three browsers and two phones, and red only when it should be. But green here means it works for one user, with a mouse, against services that never change. Would it hold 2,000 users, a customer on a keyboard, or a pricing team renaming one field? Part 5, Green Tests, Broken Checkout: The Checks Functional Tests Miss, adds those checks.
Read this with your AI