Resetting Mock State Between Test Runs

A test creates a record, a later test in another shard counts records and asserts “one”, and the count is two — because the first test’s write survived into the second. Any mock that remembers what a test did to it will leak that state across test boundaries unless you reset it at a deliberate point, and in parallel CI the leak is intermittent enough to look like random flake.

Why state bleeds and where to draw the boundary

Mocks accumulate state in three places, each needing its own reset:

  • Scenario progress. A stateful WireMock scenario advances through states as requests arrive; the next test inherits wherever the last one left the scenario.
  • Runtime overrides. MSW’s server.use() and WireMock’s dynamically-posted stubs add handlers on top of the defaults; without a reset they persist into unrelated tests.
  • Mutated data. A mock that models CRUD keeps created, updated, and deleted records in memory or a database; those mutations outlive the test that made them.

The cure is to reset at a known boundary — an afterEach for per-test isolation, or a between-shards step for parallel CI runs. This is a concrete application of mock lifecycle management, and it pairs with the stateful scenario sequences that create the state in the first place.

Solution

1. Reset a WireMock mock over the admin API

WireMock exposes two reset endpoints. Scenario reset rewinds every scenario to its start state; mapping reset discards stubs added at runtime and restores the ones loaded from disk:

# scripts/reset-wiremock.sh
#!/usr/bin/env bash
set -euo pipefail
BASE="${MOCK_BASE_URL:-http://localhost:8080}"

# Rewind all stateful scenarios to Started.
curl -fsS -X POST "${BASE}/__admin/scenarios/reset" > /dev/null

# Drop runtime stubs; keep the on-disk mappings.
curl -fsS -X POST "${BASE}/__admin/mappings/reset" > /dev/null

# Clear the recorded request journal so verifications start clean.
curl -fsS -X DELETE "${BASE}/__admin/requests" > /dev/null

echo "WireMock reset: scenarios, mappings, and request journal"

Call it between shards in CI, or from a test hook if your runner can shell out. The DELETE /__admin/requests line matters when tests assert on how many times an endpoint was called — a stale journal inflates the count.

2. Reset MSW handlers and an in-memory store

For in-process MSW, resetHandlers() removes runtime server.use() overrides. If your handlers back onto an in-memory store, clear that too — resetHandlers does not touch data your handlers mutate:

// mocks/store.ts
export interface Todo { id: string; title: string; done: boolean; }

const seed = (): Todo[] => [
  { id: '1', title: 'Write tests', done: false },
];

export let todos: Todo[] = seed();

/** Restore the store to its seeded baseline. */
export function resetStore(): void {
  todos = seed();
}
// vitest.setup.ts
import { afterEach, afterAll, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { handlers } from './mocks/handlers';
import { resetStore } from './mocks/store';

export const server = setupServer(...handlers);

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

afterEach(() => {
  server.resetHandlers();  // drop per-test overrides
  resetStore();            // drop per-test data mutations
});

afterAll(() => server.close());

Resetting both in the same afterEach guarantees each test starts from the seeded baseline regardless of what its predecessor did. Modelling that store correctly is covered in modeling CRUD state in a mock server.

3. Truncate and reseed a database-backed mock

When the mock persists to a real database (Postgres, SQLite) for fidelity, an in-memory reset is not enough — you must clear the rows and reseed:

// scripts/reset-db-mock.ts
import { Client } from 'pg';

const client = new Client({ connectionString: process.env.MOCK_DB_URL });

async function reset(): Promise<void> {
  await client.connect();
  // RESTART IDENTITY resets serial sequences so ids are reproducible.
  await client.query('TRUNCATE users, orders RESTART IDENTITY CASCADE');
  await client.query(
    `INSERT INTO users (email, name) VALUES ($1, $2)`,
    ['[email protected]', 'Seed User'],
  );
  await client.end();
  console.log('DB-backed mock reset and reseeded');
}

reset().catch((err) => { console.error(err); process.exit(1); });

RESTART IDENTITY is the detail that makes the reset reproducible — without it, auto-increment ids keep climbing and a test asserting id === 1 fails on the second run.

Verification

Prove the reset works by mutating, resetting, and re-reading in one line:

curl -fsS -X POST "${MOCK_BASE_URL}/api/todos" -d '{"title":"temp"}' -H 'content-type: application/json' && \
bash scripts/reset-wiremock.sh && \
curl -fsS "${MOCK_BASE_URL}/api/todos" | jq 'length'

Expected output is the seeded count (for example 1), not 2 — confirming the created record did not survive the reset.

Gotchas and edge cases

  • resetHandlers() does not reset data. It only removes handler overrides added with server.use(). If a handler mutates a module-level array or map, that data persists until you clear it explicitly — which is why the afterEach above calls both resetHandlers() and resetStore().
  • Reset order matters for scenarios plus data. Reset scenarios before reseeding data, not after. Reseeding first and then resetting scenarios can leave a scenario pointing at a state that expects records the reseed just replaced, producing a mismatched first response.
  • A shared mock across parallel shards needs coarser reset points. Per-test afterEach reset is unsafe when several shards hit one instance, because one shard’s reset wipes another shard’s in-flight state. Either give each shard its own instance (the per-PR stack approach) or reset only at shard boundaries, never mid-test.

← Back to Ephemeral Preview Environments