Flaky Tests Diagnosis & Stabilization (Advanced)
Learn to detect flakiness causes, control time/async behavior, and make CI reliable.
Theory
Flaky tests waste engineering time and erode trust in CI.
Advanced flakiness work means:
- reproducing locally,
- identifying non-determinism sources,
- applying stabilizing techniques,
- measuring reduction in flake rate.
1) Common flakiness sources
- Race conditions (missing
await, callbacks not awaited) - Shared mutable state across tests
- Real time usage (
Date.now(), timers) without control - Randomness without a seed
- External IO (network, filesystem, real DB) without isolation
- Parallel test execution causing interference
2) Async correctness beats “waiting”
Waiting with timeouts is fragile.
Prefer:
- await the actual promise that represents completion
- use
act()/ proper lifecycle waits (framework-dependent) - coordinate async dependencies deterministically
3) Determinism checklist
To stabilize a test, ensure:
- fixed clock (fake timers)
- fixed random seed
- isolated data (fresh DB schema, transactions, cleanup)
- no dependency on execution order
Code Example (TypeScript/Jest: fake timers + seeded randomness)
// notification.ts
export function sendWelcomeEmail(
user: { email: string },
clock: () => number,
send: (email: string, subject: string) => void,
) {
const timestamp = clock();
send(user.email, `Welcome! (${timestamp})`);
}
// notification.test.ts
import { sendWelcomeEmail } from "./notification";
test("should send deterministic welcome email", () => {
// Fake clock
const fixedNow = 1710000000000;
const clock = () => fixedNow;
const sent: any[] = [];
const send = (email: string, subject: string) => sent.push({ email, subject });
sendWelcomeEmail({ email: "[email protected]" }, clock, send);
expect(sent).toEqual([
{ email: "[email protected]", subject: "Welcome! (1710000000000)" }
]);
});
Practice
- Pick a flaky test you’ve seen.
- Record:
- when it fails (timing, environment, parallelism)
- logs around failure (state + timestamps)
- Apply exactly one stabilization technique:
- control time/random
- isolate shared state
- replace “sleep/wait” with proper awaits
- Re-run with CI-like settings (parallelism) to verify.
Common pitfalls
- Retrying everything (hides real problems)
- Using delays/timeouts instead of waiting for real completion
- Shared singletons (global caches, in-memory maps) not reset between tests
- Tests that depend on DB state not cleaned between runs
Frequently Asked Questions
Why are tests passing locally but failing in CI?
Timing differences, parallelism, missing environment variables/seed data, insufficient awaits, shared state between tests, or race conditions that only appear under load.
How do I stop flakiness at the root?
Control time, randomness, and external IO; isolate tests; enforce deterministic ordering; and use retries only as a last resort for known external issues.