Skip to content
Back to skills

Flake Pattern Reference

ASecurity

Reference catalog of the eight flake patterns - async/timing, test ordering, shared parallel state, resource leaks, network, locator drift, environment variance, randomness - with detection heuristics, remediation per pattern, and the concrete code-level fixes: replacing fixed sleeps with framework auto-waits, isolating state in beforeEach fixtures, per-worker DB schemas via workerIndex, try/finally teardown, mocking network + clock at the boundary, stable role-based locators, TZ pinning, and...

  • 10 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 12, 2026
testingtypescriptgonodetestingapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 20 files and shows the line behind each finding

Scanned October 12, 2026

npx -y skills add testland/qa --skill flake-pattern-reference --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flake Pattern Reference?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Flake Pattern Reference
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/testland-flake-pattern-reference/badge)](https://www.skillsdirectory.com/skills/testland-flake-pattern-reference)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: flake-pattern-reference
description: "Reference catalog of the eight flake patterns - async/timing, test ordering, shared parallel state, resource leaks, network, locator drift, environment variance, randomness - with detection heuristics, remediation per pattern, and the concrete code-level fixes: replacing fixed sleeps with framework auto-waits, isolating state in beforeEach fixtures, per-worker DB schemas via workerIndex, try/finally teardown, mocking network + clock at the boundary, stable role-based locators, TZ pinning, and RNG seeding. Use when triaging an unknown flake to identify the category before bisecting, or when a classified flake needs the specific code change to apply."
---

# flake-pattern-reference

> **Terminology note:** "flaky test" is a practitioner-emergent term
> popularized by the Google Testing Blog ([google-causes][gtb-causes],
> [google-flaky][gtb-flaky]); ISTQB does not maintain a canonical
> entry. This catalog reflects industry-engineering consensus, not
> ISTQB authority.

[gtb-flaky]: https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html
[gtb-causes]: https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html

A flake is rarely random - it almost always falls into one of eight
recurring patterns. Identifying the pattern early shrinks the bisect
search space dramatically. This catalog is a reference, not a
workflow; the matching workflow is in
`flaky-test-quarantine`.

The Google Testing Blog observed a near-linear correlation between
test size and flakiness rate across ~4.2M tests
([google-causes][gtb-causes]) - larger tests touch more of the eight
patterns at once.

## Pattern 1: async / timing

The most common flake category in UI and integration tests.

| Signal                                                         | What's happening                                              |
|----------------------------------------------------------------|---------------------------------------------------------------|
| Fails ~5 - 20% of runs; passes when the machine is faster        | Test waits for an arbitrary `setTimeout(N)` instead of a deterministic event. |
| Fails on CI but never locally                                  | CI runners have different cold-start timings than dev laptops. |
| Fails after a dependency upgrade with no test code change       | Library's internal timing changed (e.g. Playwright auto-wait window). |

**Remediation:**

- Replace fixed sleeps with **deterministic waits** - `await
  expect(loc).toBeVisible()`, `page.waitForLoadState('networkidle')`,
  `page.waitForFunction(...)`, etc.
- For animations, **disable** them in test setup
  (`animations: 'disabled'` in Playwright; `Cypress.config('animationDistanceThreshold', 0)` in Cypress).
- For absolute clock dependencies, **freeze time** with
  `sinon.useFakeTimers()` / `vi.useFakeTimers()` / Playwright's
  `page.clock.install()`.

## Pattern 2: test ordering

Tests pass alone, fail when run with siblings.

| Signal                                                       | What's happening                                          |
|--------------------------------------------------------------|-----------------------------------------------------------|
| `npm test -- --testNamePattern='^X$'` passes; full run fails  | Test relies on state from a previously-run test.          |
| Adding a new test breaks an unrelated existing one            | Implicit ordering dependency exposed by the new test pushing the old test into a different position. |
| Random-order test runners (Jest `randomize`) flag the suite   | Suite is order-dependent.                                 |

**Remediation:**

- Run the suite with **explicit randomization** in CI to surface
  ordering deps early (`jest --randomize`, `pytest --random-order`,
  `mocha --sort` reverse).
- Move ANY shared setup into `beforeEach` / `afterEach`, never rely on
  `beforeAll` for state that the test mutates.
- Database tests: roll back transactions after each test instead of
  truncating between describe blocks.

## Pattern 3: shared parallel state

Tests pass sequentially, fail when run in parallel workers.

| Signal                                                          | What's happening                                           |
|-----------------------------------------------------------------|------------------------------------------------------------|
| Fails ~50% of runs in CI matrix; passes locally with `-j 1`     | Two workers writing to the same DB row / file / port.     |
| Fails more often as worker count goes up                         | Linear shared-state contention.                            |
| Error message mentions "duplicate key" / "address in use" / "file already exists" | Direct collision evidence.                                  |

**Remediation:**

- Find the shared state two workers collide on - DB rows, files, ports,
  or global module state.
- Per-worker isolation: per-worker DB schemas (`PG_SCHEMA=test_${WORKER}`),
  per-worker temp dirs (`TMPDIR=/tmp/test-${WORKER}`), per-worker port
  ranges.
- For unique IDs: use UUIDs or a per-worker namespace prefix, not
  auto-increment integers shared across workers.

## Pattern 4: resource leaks

Tests pass on a fresh machine, fail after the test process has run for
hours.

| Signal                                                         | What's happening                                            |
|----------------------------------------------------------------|-------------------------------------------------------------|
| Fails increasingly often as suite duration grows                | Memory or file-descriptor leak in the test setup.          |
| `EMFILE` / `EADDRINUSE` errors mid-suite                         | File-descriptor or port exhaustion.                         |
| Long-running processes (Playwright browsers, Cypress runners) crash mid-suite | Process accumulating zombies. |

**Remediation:**

- Always `await browser.close()` / `await server.close()` in `afterAll`,
  with a `try/finally` so failed tests still clean up.
- Set per-test timeouts and ensure the framework kills the process,
  not just the test (`--testTimeout`, `test.setTimeout()`).
- Run `lsof | wc -l` and `ps aux | wc -l` before / after the suite in
  CI to detect leaks; alert when growth exceeds a threshold.

## Pattern 5: network / external service

Tests pass when the upstream is healthy, fail otherwise.

| Signal                                                         | What's happening                                            |
|----------------------------------------------------------------|-------------------------------------------------------------|
| Fails on the same handful of tests that hit the same external URL | Real network call to a flaky third party.                |
| Fails right after a deploy of a non-test service               | Test is hitting prod / staging of a sibling service.        |
| `ETIMEDOUT` / `ECONNRESET` in error logs                         | Network-layer error, not test-logic error.                  |

**Remediation:**

- **Mock at the boundary** - never let test code reach a real network
  endpoint. Use Mock Service Worker (MSW), nock, WireMock, or
  Playwright's `page.route()`.
- For tests that *must* hit a real service (smoke / contract tests),
  isolate them in a separate suite that doesn't gate the main CI.
- DNS-level: pin to specific resolvers in CI to avoid resolver
  variance.

## Pattern 6: locator drift

UI tests pass when the page looks one way, fail when it shifts.

| Signal                                                         | What's happening                                            |
|----------------------------------------------------------------|-------------------------------------------------------------|
| Fails after an unrelated CSS change                             | Selector matched by position rather than identity.          |
| `selector matched 2 elements` errors                            | Ambiguous selector now matches more than one node.          |
| Fails only at certain viewports                                 | Layout shifts cause mobile / desktop selectors to differ.   |

**Remediation:**

- Use **role-based selectors** first (`page.getByRole('button', { name: 'Submit' })`),
  then `data-testid`, only `text=` / CSS as a last resort.
- For Playwright: enable `strict: true` so any ambiguous selector
  fails immediately rather than silently picking the first match.
- For viewport-specific UIs: snapshot at every breakpoint via the
  breakpoint matrix in `playwright-snapshots`; visual signal exposes
  layout-shift flakes faster than text checks.

## Pattern 7: environment variance

Tests pass on Linux CI, fail on macOS dev machines (or vice versa).

| Signal                                                         | What's happening                                            |
|----------------------------------------------------------------|-------------------------------------------------------------|
| Fails only on a specific CI runner / OS                         | OS-specific path separator, line ending, or filesystem case sensitivity. |
| Snapshot tests fail with sub-pixel diffs across OS              | OS font / anti-aliasing differences (see `playwright-snapshots`). |
| Fails in `tz` configurations not set to `UTC`                   | Timezone-sensitive assertion.                               |

**Remediation:**

- Pin CI to one OS / one timezone (`TZ=UTC`) for deterministic runs.
- Run snapshot updates only in CI, never from a developer laptop
  (per `playwright-snapshots`).
- For path-sensitive code, normalize with `path.posix.join()` /
  `node:path`.

## Pattern 8: randomness

Tests use random data without a controlled seed.

| Signal                                                         | What's happening                                            |
|----------------------------------------------------------------|-------------------------------------------------------------|
| Failures don't reproduce on retry                               | Test data was randomized; the failing combination is gone.  |
| Test asserts a property that holds "almost always"              | Property-based test exposing a real edge case (this is good - fix the production bug). |
| Faker-generated data triggers a layout overflow                 | Random string longer than the assertion expected.           |

**Remediation:**

- Seed every random source: `Math.random` via `seedrandom`, faker via
  `faker.seed(N)`, property-based testing via `fc.assert(prop, { seed })`.
- For property-based failures, **don't** mark them as flake - copy the
  failing seed into a regression test.
- Persist the seed used in each CI run as a build artifact so a flake
  can be replayed.

## Triage decision tree

```
Test fails ~50% of runs?
├── Yes → likely "shared parallel state" or "test ordering"
└── No → fails ~5-20% of runs?
    ├── Yes → likely "async/timing" or "network"
    └── No → fails only on specific OS / runner?
        ├── Yes → "environment variance"
        └── No → fails after long suite duration?
            ├── Yes → "resource leaks"
            └── No → fails after unrelated UI change?
                ├── Yes → "locator drift"
                └── No → does the test use random data?
                    ├── Yes → "randomness"
                    └── No → run a structured bisect
```

For systematic bisection, run a structured bisect that varies one axis
at a time per the patterns above.

## Code-level fixes

Once the pattern is named - by inspection above or by experiment via
`flake-axis-bisection` - apply the smallest change the pattern calls for
(a targeted edit, not a rewrite), then re-measure at a real depth: re-run
at an N chosen from the failure rate you are willing to ship, not the
screening N; a clean 0/20 does not prove the flake is gone
(`flake-axis-bisection`). Quarantine via `flaky-test-quarantine` if the
flake blocks the trunk while the fix is in review.

The per-pattern code fixes, grounded in Playwright, Cypress, MSW, and
Faker official docs:

- **Patterns 1-4** (async/timing, test ordering, shared parallel state,
  resource leaks):
  [references/timing-and-isolation-fixes.md](references/timing-and-isolation-fixes.md).
- **Patterns 5-6** (network mock-at-the-boundary with `page.route()` /
  MSW; role-based locators with `data-testid` fallback):
  [references/network-and-locator-fixes.md](references/network-and-locator-fixes.md).
- **Patterns 7-8** (pin `TZ=UTC`, freeze the clock, normalize paths;
  seed every RNG and persist the seed):
  [references/environment-and-randomness-fixes.md](references/environment-and-randomness-fixes.md).

### Worked example

A checkout test, `tests/checkout.spec.ts:42`, fails about 15% of runs in
CI and always passes locally.

1. **Classify.** `flake-axis-bisection` implicates the network-latency
   axis, and reading the source shows the assertion is gated on a fixed
   `page.waitForTimeout(2000)`, not on the response. That is Pattern 1
   (async / timing): the sleep is shorter than the slowest CI response.
2. **Apply the fix.** Replace the fixed sleep with a web-first assertion
   that retries until the condition holds:

```typescript
// Before - the 2s sleep races a variable-latency XHR
await page.getByRole('button', { name: 'Place order' }).click();
await page.waitForTimeout(2000);
expect(await page.getByText('Order confirmed').isVisible()).toBe(true);

// After - retries until the confirmation renders or the timeout expires
await page.getByRole('button', { name: 'Place order' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
```

3. **Re-measure and ship.** The team's tolerance is 1%, so re-run at
   N=300 (`flake-axis-bisection` Step 2). A clean 0/300 bounds the rate at
   roughly 1%; a clean 0/20 would have proved nothing. No quarantine was
   needed - the fix landed inside the PR the flake was blocking.

### Quick-reference: pattern to fix

| Pattern | Key fix | Primary API |
|---------|---------|-------------|
| async / timing | Replace sleep with auto-wait assertion | `await expect(loc).toBeVisible()` |
| test ordering | Move setup to `beforeEach`; roll back DB per test | `test.beforeEach` / `test.afterEach` |
| shared parallel state | Per-worker schema / dir / port via `workerIndex` | `testInfo.workerIndex` |
| resource leaks | `browser.close()` in `afterAll` with `try/finally` | `test.afterAll` + `try/finally` |
| network | Mock at boundary; never reach real endpoints | `page.route()` / MSW |
| locator drift | Role-based locators; `data-testid` fallback | `getByRole()` |
| environment variance | Pin `TZ=UTC`; freeze clock; normalize paths | `page.clock.install()` |
| randomness | Seed every RNG; persist seed in CI log | `faker.seed(N)` |

Per-fix citations live in the three reference files above.

## References

- [google-flaky][gtb-flaky] - Google Testing Blog overview.
- [google-causes][gtb-causes] - Google's correlation analysis on
  ~4.2M tests; "test size correlates with flakiness rate."
- `flaky-test-quarantine` - 
  workflow that uses this catalog during triage.

Files in this skill

  • .tessl-plugin/plugin.json795 B
  • SKILL.md15 KB
  • evals/api-test-port-collision/criteria.json4 KB
  • evals/api-test-port-collision/task.md4.2 KB
  • evals/archive-write-race/criteria.json3.9 KB
  • evals/archive-write-race/task.md3.3 KB
  • evals/due-date-boundary/criteria.json4.6 KB
  • evals/due-date-boundary/task.md3.1 KB
  • evals/import-job-fixed-wait/criteria.json3.7 KB
  • evals/import-job-fixed-wait/task.md3.6 KB
  • evals/inventory-fixture-collision/criteria.json4.4 KB
  • evals/inventory-fixture-collision/task.md3.2 KB
  • evals/leaked-clock-stub/criteria.json4.3 KB
  • evals/leaked-clock-stub/task.md3.6 KB
  • evals/profile-completion-order/criteria.json3.8 KB
  • evals/profile-completion-order/task.md3.3 KB
  • evals/repository-pool-exhaustion/criteria.json3.8 KB
  • evals/repository-pool-exhaustion/task.md4.8 KB
  • evals/session-precondition-dependency/criteria.json4.3 KB
  • evals/session-precondition-dependency/task.md3.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.