ESC

Type to search the knowledge base.

Flaky Tests Common Causes

Hunt timing, shared state, order dependence, and network flake — a practical checklist to stabilize frontend test suites.

intermediate3 min read
  • testing
  • flaky-tests

A flaky test fails intermittently without code changes. Flakes destroy trust: teams retry CI until green or ignore failures. Fixing flake is product work.

Docs: Playwright flakiness, Testing Library async.

Top causes in frontend suites

1. Timing and races

// Bad
await user.click(button);
expect(screen.getByText('Saved')).toBeInTheDocument(); // not rendered yet

// Good
expect(await screen.findByText('Saved')).toBeInTheDocument();

Fixed sleep(500) still races on slow CI.

2. Shared mutable state

  • Singleton stores not reset
  • localStorage / cookies between tests
  • MSW handlers stacked without reset
  • DB rows on shared staging
afterEach(() => {
  localStorage.clear();
  cleanup();
  server.resetHandlers();
});

3. Order dependence

Test B passes only if test A left a user logged in. Always arrange full preconditions.

4. Network non-determinism

Real HTTP to staging: 500s, latency, rate limits. Prefer MSW or controlled test APIs.

5. Animation and transitions

Asserting mid-animation opacity. prefers-reduced-motion, disable animations in test CSS, or wait for final role/text.

6. Clock skew

Date-dependent UI (expires soon) without faking Date / timers.

7. Parallelism collisions

Two workers same account. Isolate with unique IDs per worker.

8. Selector ambiguity

getByText('Add') matches multiple. Use role + name; getAllBy* intentionally.

Debug protocol

  1. Reproduce with --repeat-each=20 or quarantine job.
  2. Capture trace/video (Playwright).
  3. Check whether failure is assertion vs timeout.
  4. Log seed data IDs.
  5. Fix root cause; remove retries as the long-term strategy.

Retries policy

Layer Retries
Unit 0
Component 0
E2E 0–1 only while triaging, then 0

Retries hide bugs.

Async RTL patterns

// wait for disappearance
await waitFor(() => {
  expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});

// findBy = getBy + wait
await screen.findByRole('alert');

Avoid manual loops.

Footguns

  1. waitFor with always-true condition.
  2. Snapshot updates on flake noise.
  3. Marking tests .skip permanently.
  4. Blaming “CI is slow” without traces.

Interview out-loud answer

“Flakes usually come from races, shared state, or real network. I use findBy/auto-wait, reset storage and MSW each test, isolate data, and treat retries as temporary. E2E gets traces; unit tests should never need sleeps.”

Quarantine with ownership

Move known flakes to a quarantined job that doesn’t block merge only with an owner and expiry date. Track counts; if a test is quarantined more than a sprint, delete or rewrite it. Silent quarantine forever is how suites rot.

Extra practice

Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.

Notes from real codebases

Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.

Further reading

Related guides