Testland
Browse all skills & agents

test-isolation-patterns

Pure reference catalog of test-isolation and fixture-lifecycle patterns - the four-phase test pattern (Meszaros), fixture scope (per-test / per-describe / shared / global), the Fresh-Fixture vs Shared-Fixture trade-off (Fowler), parallel-safety patterns, and cleanup discipline (afterEach / afterAll / tagged-cleanup), plus a pattern-selection guide and a worked leaking-state diagnosis. The database-isolation strategies (transaction-rollback / database-per-worker / template-database) and network / external-service stubbing live in references/. This is the architecture-tier reference, not a file-level fixture-coupling style rule. Use when designing fixture scope and isolation strategy, auditing fixture coupling or retry/wait policy, or moving a suite to parallel execution.

Install with skills.sh (any agent)

npx skills add testland/qa --skill test-isolation-patterns
View source

test-isolation-patterns

Overview

A test that fails sometimes for non-obvious reasons is non-deterministic. Per Martin Fowler - Eradicating Non-Determinism in Tests (opens in new window): "A test is non-deterministic when it passes sometimes and fails sometimes, without any noticeable change in the code, tests, or environment… Once you start ignoring a regression test failure, then that test is useless and you might as well throw it away." The dominant cause is broken isolation - one test affecting another, the environment leaking, fixtures sharing state. This catalog is the canonical reference for the isolation patterns that prevent it.

This skill is a pure reference - no execution steps. It is the catalog cited when auditing fixture coupling, retry/wait policy consistency, and CI integration health. It complements test-code-conventions §6 (which is the file-level rule against global-fixture hubs) with the cross-cutting architecture patterns. It also complements flake-pattern-reference, which catalogs flake symptoms; this skill catalogs the prevention patterns.

When to use

  • Designing a new framework - pick the fixture scope and isolation strategy.
  • Auditing an existing framework where flake-rate is rising (broken isolation drives the concurrency and test-order-dependency flake categories, together about a third of flakes per Luo et al. 2014 (opens in new window)).
  • Migrating from sequential to parallel execution - the parallel-safety patterns become load-bearing.
  • Refactoring fixture inheritance chains - apply the cleanup-discipline patterns.

How to use

  1. Frame each test in the four phases (Pattern 1). Be explicit about where Setup and Teardown live, because Phases 1 and 4 are where isolation breaks.
  2. Pick the tightest fixture scope that holds (Pattern 2). Default to per-test; escalate to per-describe or shared only when setup is measurably expensive and the group never mutates the fixture.
  3. Choose Fresh Fixture (Pattern 3), or a Persistent Fresh Fixture (transaction-rollback) when a fresh rebuild is measured too slow.
  4. For DB-backed or network-backed tests, pick an external-dependency strategy from references/database-store-isolation.md and references/network-service-isolation.md.
  5. Before enabling parallel execution, apply the parallel-safety patterns (Pattern 4): worker-scoped state, unique IDs, ephemeral paths, no global singletons.
  6. Wire teardown through the runner's afterEach by default (Pattern 5, cleanup discipline).
  7. Cross-check against the pattern-selection guide and the cross-cutting anti-patterns before sign-off.

Pattern 1 - The four-phase test pattern

Canonical source: Gerard Meszaros - xUnit Test Patterns: Refactoring Test Code (2007) (opens in new window). Referenced in the Wikipedia entry on test fixture (opens in new window).

Every test has four phases:

PhaseWhat
1. SetupEstablish the pre-conditions / fixture
2. ExerciseInteract with the System Under Test
3. VerifyDetermine whether the expected outcome was obtained
4. TeardownReturn to a clean state

Phases 1 and 4 together are fixture management. Patterns 2 through 5 below cover how to do them safely; isolating external stores and services is covered in the deep references.

Pattern 2 - Fixture scope

The framework's test runner offers three or four scopes; the team picks the tightest scope that meets the constraint.

ScopeLifecycleUse when
Per-test (function-scoped)Setup before each test; teardown after eachDefault. Maximally isolated. Slowest. Always parallel-safe.
Per-describe (class / module-scoped)Setup before the first test in the group; teardown after the lastSetup is expensive and the group of tests genuinely shares it (read-only)
Shared (session / worker-scoped)Setup once for the whole run; teardown at endSetup is unaffordable per-describe (e.g., spinning up a Docker stack) and the tests don't mutate it
Global (module-loading)Setup at module-import time; no teardownAnti-pattern in nearly all cases. Use only for truly immutable language-level fixtures (constants, configuration).

The single rule that prevents most flake: never share mutable fixtures across tests. If a fixture is mutated by any test, it must be per-test scoped.

Framework-specific scope syntax (illustrative; cite the per-framework skill for tool-specific details)

  • Jest / Vitest: beforeEach / beforeAll (per-describe by default within a describe block).
  • Playwright Test: test.beforeEach / test.beforeAll; test.use({}) for per-test config; fixtures via test.extend().
  • pytest: @pytest.fixture(scope="function" | "class" | "module" | "session").
  • JUnit 5: @BeforeEach / @BeforeAll; @TestInstance(Lifecycle.PER_CLASS).
  • RSpec: before(:each) / before(:all).

Anti-patterns

Anti-patternWhy it fails
Per-describe fixture that any test in the describe mutatesOne test fails; the next "starts" from the mutated state
Shared fixture mutated through a leaky abstraction (e.g., factory returns a shared object)Cross-test mutation without an obvious culprit; flake follows
Per-test scope for genuinely expensive setup (a 30s Docker spin-up per test)Suite time explodes; team skips tests
Global fixture for anything that has stateCannot reset between test runs; CI run pollutes the next run
Inheritance hierarchy of fixtures (BaseTestAppTestDomainTestSpecificTest)Depth-3+ chains break unpredictably (§A2)

Pattern 3 - Fresh Fixture vs Shared Fixture trade-off

Canonical source: Martin Fowler - Eradicating Non-Determinism in Tests (opens in new window).

Fowler's framing: "I prefer the former [Fresh Fixture], as it's often easier - and in particular easier to find the source of a problem… [but] rebuilding the database each time can add a lot of time to test runs, so that argues for switching to a clean-up strategy."

ApproachSetup costIsolationWhen
Fresh Fixture (rebuild from scratch every test)HighMaximumDefault; use unless measured slow
Cleanup strategy (preserve the fixture, undo changes at teardown)LowStrong if cleanup is comprehensiveWhen Fresh Fixture's cost is prohibitive
Persistent Fresh Fixture (fresh per test, persisted via transaction-rollback)LowMaximumThe pragmatic middle for DB-backed tests

The transaction-rollback pattern (Persistent Fresh Fixture): Begin a transaction at test start; do all the test's DB work inside it; rollback at test end. The database is materially unchanged across tests. The pattern works for any DB that supports transactions; integration-test frameworks like DatabaseCleaner (Ruby), pytest-django's db fixture, Spring's @Transactional test annotation all implement it. The five DB-level strategies are catalogued in references/database-store-isolation.md.

Anti-patterns

Anti-patternWhy it fails
Fresh Fixture that takes 60+ seconds per testSuite time becomes prohibitive; team skips tests
Cleanup strategy that misses one mutation surface (cache; queue; file system)Cross-test coupling through the missed surface
Transaction-rollback that doesn't actually rollback (autocommit, DDL changes)Silent state leakage
Shared Fixture documented as "immutable" but tests mutate it anywayThe documentation is unverified; flake follows

Pattern 4 - Parallel safety

Canonical source: Fowler - Eradicating Non-Determinism in Tests (opens in new window) on isolation as the parallel-safety prerequisite, plus Luo et al. FSE 2014 (opens in new window), which attributes 20% of flakes to concurrency problems (race conditions and deadlocks).

Parallel execution magnifies every isolation bug. The patterns that make parallel safe:

PatternWhat it does
Worker-scoped fixturesEach parallel worker has its own state (DB, file system path, port range)
Unique identifiers per testTest names, file paths, generated IDs include the worker ID (worker_${WORKER_ID}_user_${TEST_ID})
Ephemeral output pathsTests write to tmp/${WORKER_ID}/${TEST_ID}/ and clean up at teardown
Port range allocationEach worker gets a port range (30000 + WORKER_ID * 100) to avoid binding conflicts
No global singletonsNo process.env writes, no global config mutation, no static state
Idempotent setupRe-running the setup produces the same state (so a flaky-and-retried test isn't tainted)

Anti-patterns

Anti-patternWhy it fails
process.env.X = "..." in a test (writes to a shared global)Worker N's env-write affects worker M's reads
Hard-coded port 3000 in tests (port collisions)First worker binds; others fail
Tests writing to /tmp/test.log (path collision)Workers stomp each other's files
Test-name-based DB seeding (collides across workers if names overlap)Cross-worker state pollution
Per-test setup that does setTimeout / sleep to "let things settle"Flake source: async-wait is the largest flake category at 45% per Luo et al. 2014 (opens in new window); use proper event-based synchronisation

Pattern 5 - Cleanup discipline

Canonical source: Meszaros's xUnit Test Patterns (2007) - the Garbage-Collected Teardown vs In-line Teardown vs Implicit Teardown vs Setup Decorator patterns.

The four canonical cleanup approaches:

PatternMechanism
In-line TeardownEach test explicitly cleans up at end (last line of the test body)
Implicit TeardownafterEach / afterAll hooks the runner calls automatically
Garbage-Collected TeardownCleanup happens when the language's GC reclaims the fixture (typed in C# / Java with IDisposable / AutoCloseable)
Tagged CleanupFixture registers itself with a "cleanup queue" at setup; queue drains at suite end

Rule: Implicit Teardown via the runner's afterEach hook is the default. In-line Teardown is acceptable when the cleanup is specific to one test. Tagged Cleanup is for fixtures whose lifetime is variable (held across multiple tests, then released).

Anti-patterns

Anti-patternWhy it fails
No teardown ("the next test will clean up")Failing test orphans state; the next test fails too
Teardown that swallows errors silentlyReal cleanup failures are invisible; flake follows
Teardown that depends on test-pass state (if (test.passed) cleanup())Failing tests don't clean up; cascading flake
Teardown order-dependent on setup orderRefactoring setup breaks teardown

Worked example - diagnosing a leaking-state test

Symptom. updates a member's role passes when its file runs alone but fails intermittently in the full suite - the classic order-dependent flake (Fowler (opens in new window)).

The suite (before):

describe("member admin", () => {
  let user;
  beforeAll(() => { user = createUser({ role: "member" }); }); // per-describe scope

  it("promotes to admin", () => {
    promote(user);
    expect(user.role).toBe("admin"); // mutates the shared fixture
  });

  it("updates a member's role", () => {
    expect(user.role).toBe("member"); // fails: user was mutated above
  });
});

Diagnosis. Walk Pattern 2: user is a per-describe (beforeAll) fixture that the first test mutates. The second test inherits the mutated state. This is the Pattern 2 anti-pattern row "Per-describe fixture that any test in the describe mutates", and it violates the single rule that prevents most flake: never share mutable fixtures across tests.

Fix. Move the fixture to per-test (beforeEach) scope so each test gets a Fresh Fixture (Pattern 3):

describe("member admin", () => {
  let user;
  beforeEach(() => { user = createUser({ role: "member" }); }); // per-test scope

  it("promotes to admin", () => {
    promote(user);
    expect(user.role).toBe("admin");
  });

  it("updates a member's role", () => {
    expect(user.role).toBe("member"); // always fresh; order-independent
  });
});

Each test now starts from a clean object - maximally isolated and parallel-safe. If createUser were genuinely expensive, the middle path is Pattern 3's Persistent Fresh Fixture (transaction-rollback), not a shared mutable object.

Deep references

Once fixture scope is chosen, the two external-dependency concerns have their own deep catalogs:

Cross-cutting anti-patterns

Anti-patternWhy it fails
Implicit ordering (test B depends on test A's side effects)Per Fowler (opens in new window): "isolation… gives you more flexibility in running subsets of tests and parallelizing tests." Ordering breaks both.
Tests that "sleep until it works"Timing-fragile; async-wait is 45% of all flakes per Luo et al. 2014 (opens in new window)
Tests that read system time without overridesTests fail at midnight / DST / leap year
Tests that read random data without seedingNon-reproducible failures
Tests that depend on file-system layoutOS / CI-runner-specific failures
Tests that depend on locale / timezone of the runnerInternationalisation-dependent flake

Pattern-selection guide

ScenarioRecommended pattern
Default (unit / integration test)Per-test fixture scope + Fresh Fixture
DB-backed integration testPer-test fixture + transaction-rollback (Persistent Fresh Fixture)
Slow expensive E2E setupPer-describe Shared Fixture documented as immutable + transactional teardown
Parallel executionWorker-scoped DB + unique IDs per worker + ephemeral output paths
External service interactionStubs by default; contract tests at API surface; real-network only in smoke / canary
Multi-worker DB-heavy suiteDatabase-per-worker + template-database cloning
Mutation-heavy unit testsPer-test fixture + in-memory mock

Hand-off targets

  • Per-file fixture coupling ruletest-code-conventions §6.
  • Flake symptoms / pattern catalogflake-pattern-reference (qa-flake-triage) - symptoms; this skill is the prevention reference.
  • Quarantine a chronically flaky testflaky-test-quarantine (qa-flake-triage).
  • Stub / mock external servicesmsw-handlers, wiremock-stubs, mountebank-imposters (qa-test-data).
  • Test data construction patternstest-data-patterns (qa-test-data, sister catalog).
  • Object-model architecture patternsobject-model-patterns (sister catalog).
  • Test step granularitytest-step-design-patterns (sister catalog).

References

  • Martin Fowler - Eradicating Non-Determinism in Tests (the load-bearing reference for Fresh-vs-Shared-Fixture trade-off and the "non-deterministic test is useless" rule): https://martinfowler.com/articles/nonDeterminism.html
  • Martin Fowler - Practical Test Pyramid (cited for the in-memory-substitution anti-pattern): https://martinfowler.com/articles/practical-test-pyramid.html
  • Gerard Meszaros - xUnit Test Patterns: Refactoring Test Code (2007) - the canonical reference for the four-phase test pattern and all named fixture / teardown patterns: ISBN 978-0131495050.
  • Wikipedia - Test fixture (cites Meszaros's four-phase pattern): https://en.wikipedia.org/wiki/Test_fixture
  • Luo et al. (FSE 2014) - An Empirical Analysis of Flaky Tests (the original academic taxonomy of flake categories: 45% async-wait, 20% concurrency, 12% test-order-dependency, from 201 fixes across 51 projects) which this catalog's patterns prevent: https://mir.cs.illinois.edu/marinov/publications/LuoETAL14FlakyTestsAnalysis.pdf
  • Google Testing Blog, "Flaky Tests at Google and How We Mitigate Them" - flake prevalence (about 16% of tests show some flakiness): https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html
  • Testcontainers - https://testcontainers.com/ (the canonical containerised-DB-per-test reference)
  • Gerard Meszaros - Principles of Test Automation ("Principle: Keep Tests Independent", also known as Independent Test - the canonical statement of the test-isolation principle this catalog implements): http://xunitpatterns.com/Principles%20of%20Test%20Automation.html
  • ISTQB glossary - test fixture: https://glossary.istqb.org/en_US/term/test-fixture
  • test-code-conventions §6, flake-pattern-reference (qa-flake-triage) - companion file-level and symptom-level references.
  • object-model-patterns, test-data-patterns (qa-test-data), test-step-design-patterns - sister architecture-tier pattern catalogs.

Database and external-store isolation

View source (opens in new window)

Database and external-store isolation

Deep reference for test-isolation-patterns SKILL.md. Consult after picking fixture scope, when the system under test reads or writes a database, cache, or queue. This is the dominant source of test flake at scale; five canonical strategies, each with trade-offs.

Transaction-rollback (the default)

Each test runs in a transaction; teardown rollbacks. Works for: relational DBs with full transaction support. Doesn't work for: DDL changes, multiple DB connections, queues, caches.

Database-per-test-worker

Each parallel worker gets its own database (named app_test_worker_1, app_test_worker_2, etc.). Created once at startup; reused across tests within the worker; dropped at suite end. Works for: parallel execution with mutation-heavy tests. Cost: pre-suite setup time + N× DB storage.

Template database / pristine clone

Pre-create a template database with seed data; clone it per test (or per worker). PostgreSQL's CREATE DATABASE … TEMPLATE template_db is the canonical mechanism. Works for: tests needing complex seed state. Cost: template maintenance.

Containerised DB-per-test

Each test gets a fresh Docker container (Testcontainers (opens in new window) is the canonical library). Maximum isolation; highest cost. Works for: integration tests where the DB version / extensions / config matter. Don't use for: unit tests.

In-memory substitution

Use SQLite in-memory instead of the production DB engine. Fast; works for simple SQL. Doesn't work for: production-specific features (PostgreSQL JSON, Postgres extensions, MySQL spatial types). Cited as an anti-pattern by Fowler on integration tests (opens in new window) when the production engine has features the in-memory substitute lacks.

Anti-patterns

Anti-patternWhy it fails
Tests that mutate a shared DB without isolationCross-test coupling; the dominant source of flake at scale
In-memory substitution masking production-engine differencesTests pass locally; fail in production
Transaction-rollback for tests that do DDL (CREATE TABLE in test)DDL is auto-commit in most engines; rollback doesn't undo it
Database-per-worker without a maximum-worker limitStorage explodes; CI cost surges
Containerised DB-per-test for unit tests5-second container startup × 1000 unit tests = unworkable

Network and external-service isolation

View source (opens in new window)

Network and external-service isolation

Deep reference for test-isolation-patterns SKILL.md. Consult when a test touches a service the team does not control (third-party HTTP APIs, external endpoints). Tests should not depend on external services they don't control; three patterns cover the choice.

Stub (canned response)

Use when the test doesn't care about the network itself. Reach for a stub library - nock (opens in new window), WireMock (opens in new window), Mountebank (opens in new window) - or the msw-handlers / wiremock-stubs / mountebank-imposters skills in qa-test-data.

Contract test

Use when the test cares whether the service contract holds. Pact (opens in new window) or schemathesis (opens in new window) verify the contract rather than a canned body.

Real network call in a controlled environment

Use for a smoke / canary test in a staging tier with a dedicated test partition, where exercising the live service is the point of the test.

Anti-patterns

Anti-patternWhy it fails
Unit tests calling the real external APITests fail when the API is down; tests pass when the API silently changes
Stubs that drift from production response shapeTests pass with stubs that don't match reality
One global stub for the whole suiteTests cross-couple through the stub configuration
Contract test with no contract refreshStub goes stale; tests pass while production breaks

See also the msw-handlers, wiremock-stubs, and mountebank-imposters skills in qa-test-data for stub implementation, and the SKILL's pattern-selection guide for when each pattern applies.

Related skills

object-model-patterns

Pure reference catalog of the canonical object-model architecture patterns for test automation frameworks - Page Object Model (Fowler), Screenplay (Marcano/Palmer/Hill), Component Object, App Actions (Cypress idiom), Service Object, Repository, and Screen Object (the desktop/mobile sibling of Page Object covering Windows UIA, macOS XCTest, Linux AT-SPI, Appium / Espresso) - each with its canonical citation, when-to-use rules, refuse-to-mix anti-patterns, and a worked example. This is the architecture-tier reference - what each pattern *is* - not file-level style rules and not tool-specific configuration. Use when designing, reviewing, or migrating a test framework's object-model architecture.

test-code-conventions

Pure-reference catalog of test-code conventions: AAA structure (Arrange / Act / Assert), per-test single-responsibility, descriptive naming (`{sut}_{scenario}_{expected}`), assertion specificity, mocking rationale (state vs behavior, fake vs mock), fixture-coupling rules, and the magic-number / hard-coded-string anti-patterns; the E2E selector-priority and web-first-assertion conventions live in references/. Use as the shared rule book a test-code review cites back to, or as onboarding for what makes a test code-reviewable; to score a test's quality on weighted axes use test-design-scorecard, and for setup/teardown isolation specifically use test-isolation-patterns.

test-design-scorecard

Scores test files 1 to 5 on six design axes (AAA phase separation, single-responsibility, naming, fixture coupling, magic literals, setup time) using explicit per-level anchors that settle what separates a 2 from a 4, then turns the scores into growth-framed feedback and a per-author trend report; the per-PR and per-author rollup examples and trend-reporting conventions live in references/. Owns the scoring and the write-up only: the conventions being scored live in a separate conventions catalog such as `test-code-conventions`, and block-or-approve gating belongs to an adversarial review. Use when a test diff needs a graded coaching read rather than a merge verdict: onboarding a new engineer, a team deliberately ramping up test discipline, or a quarterly per-author trend where the output is a conversation, not a gate.

test-framework-architecture-audit

Audits an existing test automation framework across eight architecture-tier axes and bands each one PASS, WARN, or FAIL: page-object coverage and purity, base-class inheritance depth, fixture scope and coupling, helper sprawl, naming-convention drift, retry and wait consistency, documented-versus-actual convention drift, and CI integration health. Carries the numeric cut behind every band and labels which cuts are practitioner conventions rather than published standards. Measures the framework's own structure (page objects, base classes, fixtures, helpers, conventions), not the suite's tier mix or flake rate, and not the design of a framework that does not exist yet. Use when a test framework has grown for a release or more without structural review, before a major refactor, or when a team suspects its written test conventions no longer match what the code actually does.

test-framework-blueprint

Build-an-X workflow that takes an SDET from no test suite to a complete framework design in seven steps - inventory the SUT, choose runner + language, directory layout + fixture architecture, object-model decision, test data + mocking wiring, reporting + CI integration, conventions doc + review gates - producing a written framework blueprint (directory tree, fixture list, chosen patterns, CI matrix) plus an implementation order. This is the whole-framework design workflow - not the Step 2 runner-choice decision on its own, not the Step 4 object-model pattern catalog it defers to, and not the scaffolder that generates the harness skeleton once the blueprint exists. Use when designing a test automation framework from scratch or re-architecting one that grew organically.

test-step-design-patterns

Pure reference catalog of test-step design patterns at the architecture tier - step granularity (one logical action per step), abstraction layers (mechanical → page → business), step extraction rules (when to inline / when to extract to a helper / when to extract to a Page Object method), the declarative-vs-imperative phrasing rule, FIRST principles (Fast / Independent / Repeatable / Self-validating / Timely), and the AAA / Given-When-Then mapping. This is the cross-framework architecture-tier reference for what a step IS, when it should exist, and where it should live - not file-level AAA style rules and not Gherkin-specific translation. Use when designing or reviewing the step layer of a test framework - for example when writing or reviewing E2E or integration tests, when the step count per test is high, or when refactoring recorded or codegen test output into readable steps.

test-suite-health-audit

Measures an existing test suite's current state on four axes: per-file tier classification (unit / integration / E2E, first match wins), pyramid ratio against whatever target the team already committed to, per-layer flake rate, and defects-caught-per-run-minute ROI per tier, then reduces them to one categorical verdict (Healthy, Needs pruning, Needs refactor, Cannot assess). Reports severity against a target ratio but never prescribes one: choosing the target unit:integration:E2E mix and the rebalancing plan belongs to a pyramid-balancing capability such as `test-pyramid-balancer`. Use when a suite has grown for a year or more without review and someone needs a defensible read on whether it is healthy, over-grown, or structurally inverted before deciding what to delete or rewrite.