neart.ai
EcosystemStoryHow We BuildPricingBlog
Try Inspected →
neart.ai
EcosystemStoryHow We BuildBlog

Ní neart go cur le chéile

A SaltCore Group Limited company

© 2026 neart.ai · SaltCore Group Limited. All rights reserved.

Software Quality

How Do You Write UI Tests That Don't Become Flaky in the First Place?

19 November 20254 min read

The most reliable way to avoid flaky UI tests is to design them defensively from the start: use stable semantic locators, wait on conditions rather than fixed timeouts, make every test fully isolated and self-contained, control all data and time, and assert on user-visible behaviour rather than implementation detail. Fixing flakiness after the fact is far more expensive than preventing it, because intermittent failures are slow to diagnose. The good news is that the preventive habits are few and learnable.


## Use stable, semantic locators


Most locator-related flakiness comes from selectors tied to things that change.


- **Prefer dedicated test attributes** such as `data-testid`, which exist specifically to give tests a durable anchor.

- **Use ARIA roles and accessible names** where appropriate; they tend to be stable and double as accessibility checks.

- **Avoid positional XPath and auto-generated class names.** They break the moment the DOM is restructured for styling.

- **Never select by text that changes** with locale, personalisation, or copy edits, unless that text is the thing under test.


Self-healing can rescue broken locators, but a stable locator that never breaks is always better than one that needs repairing.


## Wait on conditions, never the clock


Fixed sleeps are the single largest source of timing flakiness. A two-second wait is too short on a loaded CI agent and wasteful on a fast one.


- Wait for a **specific element** to be visible and interactable.

- Wait for the **network to settle** before asserting on data-driven content.

- Wait for an **explicit state**, such as a spinner disappearing or a success message appearing.

- Wait for an element to be **actionable**, not merely present, so animations cannot cause a missed click.


Every wait should express the condition you actually depend on, so the test is as fast as possible on good runs and patient enough on bad ones.


## Isolate every test


Tests that depend on each other are a hidden time bomb that detonates the moment you parallelise or reorder.


- **Set up your own data** in each test and **tear it down** afterwards.

- **Never assume execution order.** A test must pass alone and in any sequence.

- **Use separate accounts, tenants, or namespaces** so concurrent tests cannot collide over shared state.

- **Reset application state** between tests rather than relying on leftovers from a previous run.


Isolation is what lets you run tests in parallel for speed without introducing order-dependent flakiness.


## Control data and time


Non-determinism guarantees eventual failure.


- **Freeze the clock** so date- and time-dependent logic behaves identically every run.

- **Seed random generators** so any randomness is reproducible.

- **Sort results** before asserting, since many data sources do not guarantee order.

- **Avoid asserting on values you do not control,** such as server-generated IDs or timestamps, unless you assert only on their shape.


## Assert on behaviour, not internals


Tests coupled to implementation detail break whenever the implementation changes, even when behaviour is unchanged.


- Assert what the **user would observe**: visible text, enabled buttons, navigation outcomes.

- Avoid asserting on internal CSS classes, DOM structure, or private state.

- Keep each test focused on **one behaviour**, so a failure points clearly at one cause.


## Tame external dependencies


A test that calls a live third-party service inherits that service's unreliability.


- **Mock or stub** external APIs in your main suite for determinism.

- Keep a small, clearly-labelled **integration suite** for genuinely exercising live connections, and accept it will be less stable.


## Design for the CI environment


Tests often pass locally and fail in CI because the environment differs.


- Run tests in CI-like conditions early, including realistic parallelism.

- Right-size CI resources so timeouts reflect real waits, not starved machines.

- Reserve retries strictly for infrastructure-class errors, never to paper over assertion flakiness.


## Make it a team standard


Resilient tests come from shared habits, not heroics:


1. Codify these patterns in a short testing guideline.

2. Review new tests for fixed sleeps, fragile locators, and shared state.

3. Treat any new flaky test as a defect in the test, to be fixed promptly.


neart.ai builds enterprise-grade quality products in this area, and the recurring lesson is that prevention through good authoring beats any after-the-fact remedy, including self-healing and retries.


## Takeaway


Flaky UI tests are largely preventable. Anchor on stable test IDs, wait on conditions rather than the clock, isolate every test, control data and time, and assert on visible behaviour. Bake these into a shared standard and review against it, so flakiness is caught at authoring time instead of debugged painfully in production pipelines.

Related posts

Software Quality

How Do You Ship Software Without Regressions?

Software Quality

What Is End-to-End Testing, and When Should You Use It?

Software Quality

What Does WCAG 2.2 Mean for Your Testing Strategy?