Mocking and service virtualisation
MSW, WireMock, and knowing when a fake helps versus when it quietly makes the test meaningless. The judgement call — what to stub and what to leave real — matters far more than the tool.
A test double is anything standing in for a real dependency. Three kinds get confused constantly, and the distinction is useful.
A stub returns canned answers. Ask it for a payment result, it says success. It has no opinion about how it was called.
A mock also asserts on the interaction. It fails the test if the call was not made, or was made with the wrong arguments.
A fake is a working lightweight implementation. An in-memory database, or a payment service that actually tracks balances.
Service virtualisation is the same idea at service level: a running process pretending to be a third party, with configurable responses, latency and failures. WireMock and Mock Service Worker are the common tools.
The terms you will hear
- Test double. The umbrella term for stubs, mocks, fakes and spies.
- Stub. Returns a fixed response.
- Mock. Verifies how it was called, as well as responding.
- Fake. A real but simplified implementation.
- Spy. Records calls without changing behaviour.
- Service virtualisation. A configurable stand-in service, over HTTP.
- Drift. The double and the real dependency no longer agreeing.
What to double, and what not to
Double these: third parties you do not control, anything charging money, anything slow, anything with rate limits, anything that cannot produce the failure you need on demand.
Prefer real for these: your own database in integration tests, your own code, anything where the double would end up reimplementing the logic under test.
The test to apply: if the double has to contain business rules to be useful, you are about to test the double. Use a fake or the real thing instead.
The drift problem
A double is a snapshot of your understanding of a dependency. Dependencies change.
For example, a payment provider adds a required field, or starts returning null where it used to return an empty array, or changes an error code. Your stub keeps returning the old shape. The suite stays green. Production breaks.
Three defences, in order of cost:
- Contract tests with the provider, where the provider participates. The strongest option, covered in contract testing.
- A small scheduled suite against the real sandbox. Cheap, and it catches shape changes within a day.
- Recorded real responses used as the stub payloads, refreshed on a schedule so the drift is visible in a diff.
How to use doubles well
- Double at the boundary, not inside. Intercept the HTTP call, rather than replacing your own client class. Then your own code stays under test.
- Use real recorded payloads. A hand-written stub body reflects what you assumed. A recorded one reflects what the service said.
- Simulate the failures you care about. Timeout, 500, 429 with a retry-after, malformed JSON, slow response. These paths are where real incidents come from and they are almost never tested.
- Assert on the call where it matters. For a payment, verifying the amount and the idempotency key were correct is the point of the test.
- Keep one fake rather than many stubs where behaviour matters. A small in-memory payment fake that tracks balances beats forty stubbed responses.
- Refresh the payloads on a schedule, and treat a diff as a change worth reading.
- Never let a double decide a release. Keep the scheduled real-sandbox suite, per determinism.
A worked setup
For example, here is the payment dependency handled properly.
DEPENDENCY Stripe. We do not own it, it charges money, and it cannot
produce a timeout on demand.
UNIT TESTS our own calculator, no doubles at all.
the maths is ours, so doubling it would test nothing.
INTEGRATION TESTS HTTP intercepted at the boundary with recorded payloads
happy path recorded from the sandbox 2026-08-01
card declined recorded
3D secure needed recorded
429 rate limited hand-built, with retry-after: 2
timeout simulated by delaying 30s
malformed body hand-built: valid 200, body is "OK" not JSON
null paymentLines recorded after the provider change in July
assertions include the call itself, not only the response:
amount sent is 1099, not 10.99 (the unit mistake)
idempotency key present and stable (the double-charge guard)
FAKE a 60-line in-memory payment service used by the checkout journey
tests. tracks balances, so a double redemption genuinely fails.
chosen over stubs because the behaviour, not the response, is
what those tests exercise.
REAL SANDBOX 6 tests, nightly, not on the blocking path
buy, refund, partial refund, declined card, 3DS, webhook receipt
purpose: catch drift within a day.
WHAT DRIFT COST US BEFORE THIS
the provider made paymentLines nullable in July. our stub returned
an array. suite green for three weeks. the mobile app crashed in
production. the nightly sandbox suite now catches that class in
under 24 hours.The two details worth stealing: assertions on the request as well as the response, and recorded payloads with a date on them so drift shows up as a diff.
How to show you know it
- A failure matrix. Timeout, 500, 429, malformed. Most suites have none of these, and incidents come from exactly there.
- A request assertion. Checking the amount and the idempotency key demonstrates you understand what a mock is for.
- A drift story. "The provider made a field nullable and our stub did not, so here is the nightly suite that now catches it."
- A fake you chose over stubs. Explaining when behaviour matters more than a canned response is the senior distinction here.
Questions
Is mocking bad practice?
No, over-mocking is. Double what you do not own and cannot control, keep your own logic real, and be suspicious of any test where the doubles outnumber the real objects.
Where should I intercept, in the code or over HTTP?
Over HTTP where you can. Replacing your own client class removes that client from test coverage, and it is usually the part with the retry and parsing logic in it.
How often should recorded payloads be refreshed?
Often enough that a change surfaces in days rather than months. Monthly is reasonable for a stable provider, and a nightly real-sandbox suite is the cheaper insurance.
Do doubles remove the need for contract testing?
The opposite. Doubles create the drift risk, and contracts or a real-sandbox suite are what manage it. A stub with no check against reality is a guess with a green tick next to it.