4 min readtest-datae2eplaywrightisolation

Test data management for E2E suites: stop sharing state, start isolating it

TestRelic Team

There's a category of test failure that no trace viewer can explain: the test is fine, the app is fine, but the data was wrong when the test arrived. The shared staging account already had an item in its cart. The coupon code got consumed by yesterday's nightly run. Two CI shards grabbed the same user at the same time. These failures get filed as "flaky," retried until green, and never actually fixed — because the cause isn't visible in any single run.

Test data management is the least glamorous of the classic automation testing challenges, and one of the most expensive to ignore. Here's a practical way through it for E2E suites.

The failure modes, named

Almost every data-caused failure is one of four:

  1. Shared mutable state. Many tests, one seeded account. Any test that mutates it (adds to cart, changes a setting) poisons the well for whoever runs next. Symptom: tests pass alone, fail in the suite — and fail differently depending on execution order.
  2. Leftover state. Cleanup that runs in afterEach doesn't run when the test crashes before reaching it. State accumulates across runs until something tips over. Symptom: the suite degrades over days, then "fixes itself" when someone reseeds staging.
  3. Parallel collisions. Playwright runs workers in parallel; two workers using the same login are a race by construction. Symptom: failures correlate with worker count and disappear with --workers=1.
  4. Drifting environments. The data in staging simply isn't what the test assumes — someone demoed on it, a migration reshaped it. Symptom: a whole class of tests breaks with no corresponding code change.

Note what these share: the failing test is rarely the guilty test. The assertion that fails is downstream of the mutation that caused it. That's why per-failure debugging keeps missing it.

Isolation patterns that hold up

The principle is boring and non-negotiable: every test owns its data. In rough order of preference:

  • Create per-test, via API. The test (or a fixture) creates its own user/order/project through the app's API in setup, uses it, and doesn't care about cleanup beyond politeness. Unique per worker and per run — Playwright's testInfo.parallelIndex and a run ID make collision-free naming trivial. This is the default posture to aim for.
  • Worker-scoped accounts. When account creation is expensive, give each parallel worker its own account via a worker-scoped fixture. Workers never share; tests within a worker run serially against it.
  • Reset, don't clean up. Prefer "reset to known state in setup" over "clean up in teardown" — setup always runs, teardown only usually runs.
  • Mock the third parties. Payment sandboxes and external APIs you don't control are shared mutable state you can't fix. Use Playwright's network interception at the boundary, and keep a small number of unmocked smoke tests to catch real integration drift.
  • Quarantine the un-isolatable. Some flows (admin settings, billing plan changes) genuinely mutate global state. Mark them, run them serially in their own project, never in the parallel pool.

Diagnosing data coupling from history

Suspecting data coupling is easy; proving it requires looking across runs, because the signature is relational:

  • The failure rate changes with parallelism, not with deploys.
  • The same test fails with different errors depending on which tests preceded it.
  • Failures cluster in time (after nightly runs, after a demo) rather than after commits.

This is exactly the shape of question accumulated run history answers. With every run uploaded by the Playwright reporter into the dashboard, you can see a test's failures across weeks with their CI metadata side by side — and the captured API call data shows the actual payloads: the cart that "should" have been empty visibly wasn't, in the response body, at setup time. Ask AI shortens the loop further — "do the failures of checkout.spec.ts correlate with worker count or with time of day?" is a one-line question over data you already have.

An underrated companion: monitoring views with distinct cadences. When the nightly run leaves a mess, it's the morning smoke run that pays — seeing those as separate lanes with separate baselines makes "nightly poisons morning" a visible pattern instead of folklore.

A migration plan that doesn't boil the ocean

Retrofitting isolation into a suite of hundreds of tests is a slog if you do it alphabetically. Do it by evidence instead:

  1. From run history, list the tests whose failures pattern-match data coupling (order-dependent, parallelism-sensitive, time-clustered).
  2. Fix the worst offender's data source, not the test — one shared account usually feeds dozens of tests, so one per-worker fixture fixes them all at once.
  3. Add a rule for new tests: no test references a hard-coded account, email, or entity name. Fixtures only.
  4. Watch the flake rate for the migrated group in history. Data fixes show up as a step change, not a gradual drift — if the rate didn't drop, you fixed the wrong thing.

Shared state is technical debt with compound interest: every new test written against a shared account raises the cost of eventually fixing it. The teams that get this right aren't the ones with the fanciest seeding framework — they're the ones where "every test owns its data" stopped being an aspiration and became a review rule.