Mock servers for QA: WireMock and friends — when to replace a dependency with a stub
Last time we talked about Testcontainers — how to spin up real dependencies in a test. Today the flip side: when a dependency can’t or shouldn’t be run for real, you replace it with a mock server. It’s an HTTP stub that responds exactly as you programmed: the status, body, delay, error you need.
The key is to understand when a mock is appropriate, what it should be able to do, and where it lies.
Why mock
- Third-party / paid API: payments, cards, SMS, weather — hitting the real one is expensive, rate-limited, or simply not allowed in CI.
- Unstable dependency: an external service goes down/slow — your tests shouldn’t flake because of it.
- The service doesn’t exist yet: frontend/QA work in parallel with the backend against an agreed contract.
- Hard responses: 429, 503, timeout, malformed body, partial response — can’t reproduce on demand for real, but trivial to set in a mock.
What a good mock server should do
- Request matching: the stub triggers on method, path, headers, query, body (exact/templated/regex).
- Fault injection: delays, error codes, connection drops, garbage bodies — to test the client’s resilience.
- Stateful scenarios: the response changes step by step (created → now exists), state emulation.
- Record & replay: record real traffic and replay it as a mock (a starting stub set from prod).
- Verify: assert the client actually made the expected call (with the right body/headers).
- Templating: a dynamic response based on the request (echo id, current time).
Tools and their niches
- WireMock — powerful (JVM, standalone/Docker available): rich matching, fault injection, proxy+record, verify. The workhorse for integration tests.
- MockServer — flexible expectations, proxying, on the JVM; a close competitor to WireMock.
- Mockoon — desktop GUI, instant no-code start; handy for manual QA and quick stubs.
- Prism — a mock straight from an OpenAPI spec: the stub automatically matches the contract (and validates requests against it).
- Hoverfly — a lightweight proxy with record/replay (capture/simulate), great for capturing real traffic.
Mock or a real dependency (Testcontainers)?
A simple rule:
- Real (Testcontainers) — what’s yours and spins up locally: DB, broker, cache, your own service. A mock here only hides real integration bugs.
- Mock — external and out of your control: third-party/paid APIs, services across the network boundary, things that don’t exist yet, and rare failure responses.
Don’t mock what you can run for real. A mock of your own DB is a test of the stub, not the system.
The main risk of mocking — contract drift
The stub freezes: the real API changed its format/codes, but your mock still answers the old way. Tests are green, prod breaks. How to hedge:
- Contract testing (Pact): provider and consumer verify the contract’s consistency automatically.
- Mock from the spec: Prism generates a stub from OpenAPI — it won’t diverge from the schema as long as the schema is current.
- A periodic smoke against the real API (in a separate run, not every PR).
- Record-from-real: regenerate stubs from fresh real traffic.
A mock as a resilience-testing tool
The most underrated benefit: a mock trivially reproduces what you can’t trigger for real on demand.
- Timeout and slow response → test client timeouts and retries.
- 429/503 → test backoff, circuit breaker, graceful degradation.
- Malformed/partial body → parsing doesn’t crash, the error is handled.
- Connection drop mid-response → the client doesn’t hang.
Common mistakes
- The mock is too rosy: always 200, never errors — the client isn’t tested for failures.
- Mocking what you could run for real (your own DB/service).
- No
verify: the test is green even if the client never made the call. - Stubs are stale and have diverged from the real API (contract drift).
- Happy-path only, no 4xx/5xx/timeouts.
- Hardcoded ports/hosts instead of a configurable base URL.
Mocking checklist (10 points)
- Only the external/uncontrolled is mocked; your own is run for real.
- Request matching is strict enough (method/path/body/headers).
- There are negative stubs: 4xx, 5xx, timeout, malformed body.
- verify checks the client made the expected call.
- The contract is protected: Pact / mock from OpenAPI / record from real.
- There’s a periodic smoke against the real API.
- Stateful scenarios where the response depends on previous steps.
- The dependency’s base URL is configurable (easy to switch mock/real).
- Stubs are versioned together with the tests.
- Fault injection is used to test client resilience.
Bottom line
A mock server is about control: you define exactly the responses you need, including failures. But that’s precisely why a mock easily makes tests too green and diverges from reality. Mock the external, keep the contract protected (Pact/OpenAPI/real smoke), always test errors, and check calls via verify. Real infrastructure — via Testcontainers, not a stub.