A stub that returns 200 for every call is useful for connectivity, but it is a weak model of an integration. Real dependencies change state: an order moves from pending to paid, a token expires, or a retry succeeds after a temporary error. WireMock can model those transitions and record the requests it receives. Used together, stateful stubbing and request verification let a test check both halves of a conversation: how the dependency responded and what the system under test actually sent.
The blind spot in static stubs
Imagine a checkout service that creates a payment and then polls its status. Two static mappings can return plausible JSON, yet the test may still pass if checkout polls the wrong payment ID, omits an idempotency header, or performs five unnecessary requests. A response-only assertion sees the final checkout state; it does not prove that the integration protocol was followed.
A useful service double should be strict about the behavior the application depends on, but tolerant of irrelevant variation. Match the method, path, business-critical headers, and meaningful body fields. Avoid matching volatile headers, JSON property order, or a trace ID unless they are part of the contract. Excessively broad matching creates false positives; excessively exact matching couples tests to noise.
- Stub matching decides which simulated response a request receives.
- Request verification checks what arrived after the application has run.
- Application assertions check the observable outcome in the system under test.
Model a workflow as an explicit state machine
WireMock scenarios give named mappings a current state. A scenario begins in a defined starting state; a matched request can move it to the next state, and later mappings can require that new state. This is a compact way to express a dependency workflow without building a fake application with its own database.
For a payment example, POST /payments can return an identifier and transition the scenario from Started to Created. The first GET /payments/pay-42 can return pending and transition to Settled. A subsequent GET can then return paid. The sequence is visible in configuration, so a reviewer can understand why each response is possible.
{
"scenarioName": "payment lifecycle",
"requiredScenarioState": "Started",
"newScenarioState": "Created",
"request": {
"method": "POST",
"urlPath": "/payments",
"bodyPatterns": [{"matchesJsonPath": "$.amount"}]
},
"response": {
"status": 201,
"jsonBody": {"id": "pay-42", "status": "pending"}
}
}Verify intent after the workflow
WireMock keeps a request journal unless it is disabled. After checkout finishes, the test can verify that POST /payments was received exactly once, that the amount and currency were correct, and that polling used /payments/pay-42. Cardinality matters: at least once may be correct for an eventually consistent notification, while exactly once is important for a non-idempotent operation.
Verification is not a replacement for outcome assertions. If the test only checks that a request was sent, the application could ignore a valid response and still pass. A stronger test triangulates the result: verify the outbound interaction, assert the final checkout state, and inspect the dependency response chosen by the scenario when diagnosis is needed.
- Verify business-significant body fields with JSON-aware matching instead of raw string equality.
- Check zero calls for paths that must not be reached, such as a capture request after authorization fails.
- Use exact call counts only where the protocol or side effects make the count meaningful.
Isolation is part of the design
State makes a mock more expressive and creates a new cleanup obligation. Reset scenario state and the request journal before each test, or give every test a unique scenario and resource identifier. Otherwise one test may leave a scenario in Settled and make the next test skip the behavior it intended to exercise.
Parallel execution needs special care. A shared WireMock instance with a shared scenario name is mutable global state. Options include one WireMock process per worker, unique scenario names generated from the test ID, or grouping stateful tests in a serial lane. The right choice depends on startup cost and suite size, but the isolation rule should be explicit rather than accidental.
Know where the double stops being evidence
WireMock can prove that the application handles the behavior encoded in its mappings. It cannot prove that a real provider implements that behavior, that TLS and network policies are correct, or that the provider has not changed. Keep a smaller set of provider-facing contract or integration checks, and use stateful doubles for fast, controlled branches that are difficult to reproduce against a shared environment.
The best mappings are reviewed like test code. They name the business state, represent only required provider behavior, and fail clearly when an unexpected request arrives. That turns a mock from a convenient response dispenser into an executable model of the integration.
Practical takeaways
What to carry into the next test suite
- Use scenarios when later responses genuinely depend on earlier calls.
- Treat matching, verification, and application assertions as three different checks.
- Reset state and request history deliberately, especially in parallel suites.
- Retain real-provider checks because a service double validates only its encoded model.
References
Primary documentation and technical references used in this article.