toolsmockingapicontract-testingtesting

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)

  1. Only the external/uncontrolled is mocked; your own is run for real.
  2. Request matching is strict enough (method/path/body/headers).
  3. There are negative stubs: 4xx, 5xx, timeout, malformed body.
  4. verify checks the client made the expected call.
  5. The contract is protected: Pact / mock from OpenAPI / record from real.
  6. There’s a periodic smoke against the real API.
  7. Stateful scenarios where the response depends on previous steps.
  8. The dependency’s base URL is configurable (easy to switch mock/real).
  9. Stubs are versioned together with the tests.
  10. 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.

Sources