Quick answer: Mock mappings accumulate like any other code, and over time a test suite builds up dead stubs, mappings no test actually exercises. They look harmless but increase maintenance, hide what the mock really does, and can mask broken behavior because a stub exists even though nothing proves it works. The fix is to treat mock mappings as covered code: record which stubs were hit during a run, and fail or report on mappings that were never matched. A mock where every stub is verified by a test is a mock the team can trust, and pruning dead mappings is part of keeping it that way.
Dead stubs arise naturally. A test is renamed or removed, a stub is added for a scenario that later changes, and the mapping is left behind because deleting it is never urgent. Individually each one is small, but they accumulate into a mock configuration that says more about history than the current system. Worse, a stale stub can still match a request and return a response that no longer reflects the API, giving a false green. The engineering discipline is to make stub usage observable, so the team knows which mappings earn their place and which are just noise.
Why dead mappings matter
The cost is indirect but real, and naming it makes the cleanup worth doing.
| Cost | How it shows up |
|---|---|
| Maintenance burden | More mappings to update on a spec change |
| False confidence | A stub exists but no test proves it |
| Hidden behavior | Stale matching returns outdated responses |
| Onboarding friction | Hard to tell which mappings are real |
| Drift surface | More places to diverge from the contract |
Record which mappings are actually hit
A mock can track every matched stub and expose the list after a run, so coverage of mappings is measured the same way code coverage is.
// wrap each mapping with a hit counter; after the run, a mapping with // zero hits is a dead stub candidate const mock = createMock(spec); const hits = new Map(); mock.onMatch((mapping) => { hits.set(mapping.id, (hits.get(mapping.id) || 0) + 1); }); // run the whole test suite await runTestSuite(mock); // report mappings no test exercised const dead = mock.mappings.filter((m) => !hits.has(m.id)); for (const m of dead) { console.log(unused mapping: ${m.id} ${m.method} ${m.path}); }
Distinguish dead from deliberately shared
Not every unhit mapping in a single run is truly dead; some shared stubs are used by suites that run separately, and some fallback mappings are intentional safety nets. Mark those explicitly rather than deleting everything unhit, so the report distinguishes "verified unused" from "declared shared." The default should be that a mapping is exercised by a test, with exceptions that are named and reviewed.
# explicit annotations keep intentional mappings honest mappings: - id: fallback-500 purpose: shared safety net for all integration suites usedBy: [orders-suite, payments-suite] - id: old-export-route # no annotation and no hits -> safe to remove
Make stub coverage part of the build
For a healthy codebase, turn unused mappings into a report first, then tighten the gate once the backlog is pruned so a new dead stub fails the build. A local-first OpenAPI workflow generates mappings from the spec and keeps the coverage check in CI, so the mock stays aligned with the contract and every mapping is traceable to a test. This is the same hygiene applied to production code, applied to the test infrastructure: if a thing cannot be proven to be used, it should be removed, and the build should be the thing that proves it.
Frequently asked questions
How often should we prune dead mappings?
Run the coverage report on every build and prune in small batches when the backlog appears. Once the mock is clean, a strict gate prevents new dead stubs from accumulating.
Could a zero-hit mapping still be needed in production-like runs?
Yes, shared or fallback mappings can be unhit in one suite while used elsewhere. Mark those with an explicit purpose and owner rather than deleting them, so the exemption is visible.
Does this apply to generated mocks or only hand-written ones?
Generated mocks benefit too, though the risk is lower because mappings derive from the spec. Coverage still proves the generated operations are actually exercised, and it can reveal spec paths no test covers.
What is the danger of leaving stale stubs beyond clutter?
A stale mapping can still match a request and return an outdated response, producing a green test against behavior the real API no longer has. That false confidence is the most serious risk and the main reason to enforce coverage.
This is part of the Powerduck vs WireMock comparison — start there for the full decision around mock servers and stub management.
Comments
Join the discussionNo comments yet. Be the first to comment.

