DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Resetting Mock State Between Tests for Real Isolation

Quick answer: Stateful mock servers are shared infrastructure, and if one test leaves stubs, counters, or scenario state behind, the next test starts from an unknown condition and fails for reasons unrelated to the change under test. Real isolation requires resetting the mock to a clean baseline between tests, either by restarting a disposable instance, invoking an explicit reset, or giving each test a unique context so it never observes another's state, and then asserting only against the state the test itself established. The dangerous shortcut is to order tests so they happen to build on each other's state, which makes the suite pass when run in sequence and break when a single test is run in isolation or tests run in parallel. If you cannot run any test on its own and get the same result, the mock is not isolated; it is a hidden dependency that produces order-dependent flakiness.

Mock servers start life feeling stateless because a simple stub matches a request and returns a fixed response, but real usage quickly introduces state: scenario-based stubs advance through states, request counters and verifications accumulate, and stubbed sequences return different responses on successive calls. Each of these is useful for modeling a service's behavior, but they turn the mock into something with memory. When tests share a long-running instance, the state created by one test is visible to the next, and the symptoms are familiar and hard to diagnose: a verification of "never called" fails because an earlier test made the call, a scenario stub is already advanced past its starting state, or a stateful sequence returns the wrong response because the counter did not start at zero. The fix is not to avoid stateful mocks; it is to make the lifecycle of that state explicit and bounded.

How state leaks across tests

Each leak corresponds to a specific piece of mock memory that was not reset.

Leaked state Symptom in the next test Isolation fix
Scenario position Stub already advanced Reset scenarios
Request counters Verified counts wrong Reset journal
Added stubs Unexpected match wins Reset mappings
Sequence position Wrong response returned Restart or reseed

The added stubs row is subtle because a more specific stub registered by an earlier test can override the baseline stub, so the test receives a response it never configured, and the failure points at the wrong code.

Reset to a known baseline

The strongest option is a disposable mock instance per test or per suite, started from a fixed configuration and torn down afterward, which guarantees no state survives. When a single instance is reused, invoke an explicit reset in the test setup so every test begins from the same baseline.

Resetting in beforeEach rather than afterEach is a deliberate choice; it guarantees the baseline even when a previous test failed or aborted before its cleanup ran, so a failed test cannot poison the rest of the run.

Prefer unique contexts over shared state

Where resetting is expensive, give each test a unique identifier, such as a distinct user id or resource prefix, and scope stubs and verifications to that id, so tests never observe each other's requests even against the same instance. This is what makes parallel execution safe.

The mock's contract should be documented in the test configuration, including which baseline stubs exist and what reset guarantees the setup provides, so a new test author knows the starting condition rather than discovering it from failures. Keeping the baseline configuration as a versioned file in the repository makes the clean state reproducible and reviewable.

Prove isolation explicitly

Run the suite with tests in a randomized order and, where possible, in parallel, and run individual tests in isolation. A correctly isolated mock gives identical results in every case; any failure under these conditions identifies a hidden state dependency. Treat this as a design requirement rather than an occasional manual check, because order-dependent tests are among the most expensive flaky failures to diagnose.

The practical rule is to treat the mock's state as something each test must own and return to a clean baseline through disposable instances, explicit resets, or unique contexts. Ordering tests to build on each other's state is not a valid design; it is a hidden coupling that breaks under parallel or isolated runs. Reset in setup, scope interactions per test, and verify by randomizing order, and the mock becomes a reliable, deterministic dependency instead of the source of order-dependent failures.

Frequently asked questions

Why do my tests pass in sequence but fail alone?

Because later tests rely on state created by earlier ones. A test that cannot run in isolation is coupled to shared mock state, and the fix is to reset or scope that state.

Is it better to reset in setup or teardown?

Reset in beforeEach so the baseline is guaranteed even if a prior test failed and skipped its cleanup. Teardown-only resets can be bypassed by an aborted test.

How do I run mock tests in parallel safely?

Use a disposable instance per test or give each test a unique context id and scope stubs and verifications to it, so interactions never cross.

What is the risk of leaving extra stubs registered?

A more specific leftover stub can override the baseline mapping and return a response the test never configured, producing a failure that points at the wrong code.

How can I prove the mock is truly isolated?

Run tests in randomized order and in parallel, and run individual tests alone. Identical results in all cases confirm no hidden state dependency remains.

This is part of the Powerduck vs WireMock comparison — start there for the full picture of mock server choices.

Top comments (0)