Articles
WireMock Interview Questions: Request Matching, Stateful Scenarios, and Verification
Prepare for WireMock interviews with matcher priority, scenario state, request verification, reset boundaries, and a realistic client-test exercise.
Your test says the payment API returned 202 Accepted, but a different test receives the same response for an invalid order. Both tests are green until production rejects the request. The failure may not be in application code: a broad WireMock stub can intercept a request intended for a more specific stub. In a WireMock interview, the real skill is making the mocked HTTP contract observable: which request matched, which scenario state was active, and what the client actually sent. This guide builds one original order-reservation exercise around those three questions.
Evidence boundary: WireMock's official documentation supports the matcher, stub priority, scenario, request-journal, and JUnit behaviors cited below. The order service, examples, and grading rubric are original editorial exercises. Exact extension defaults should be checked against the version used by your test suite.
The exercise: distinguish the right request from a plausible one
Assume an order service calls POST /reservations on a warehouse API. A valid request has JSON body {"orderId":"123","sku":"A-1","quantity":2} and header X-Tenant: north. The first accepted reservation returns 202 with a reservation ID. A repeated status check returns an accepted state. An invalid quantity returns 400. The interviewer asks for a test double that makes these paths deterministic, verifies the outgoing request, and cannot leak state into the next test.
Begin with the contract you need to exercise. A matcher for only the URL path is probably too broad: an invalid quantity, wrong tenant, or unexpected HTTP method could all receive the happy-path response. WireMock request matching can match method, URL, headers, and JSON body conditions. Use only the fields that define this test's relevant contract. Matching every incidental header and whitespace character makes the test brittle; matching nothing but /reservations makes it blind.
| Stub | Minimum relevant match | Response | Why it exists |
|---|---|---|---|
| Valid north-tenant reservation in this fixture | POST /reservations, tenant header, order ID, SKU, quantity 2 | 202, reservation ID | Prove this expected request shape |
| Invalid quantity | Same method/path, quantity zero | 400, validation body | Exercise client error handling |
| Catch-all unexpected call | Broader request or unmatched-request inspection | Explicit diagnostic failure | Make a missing contract visible |
The catch-all should not accidentally overshadow a specific stub. WireMock stub priority lets a lower numeric priority win when mappings overlap. With equal priority, the most recently added matching stub is chosen by default. A candidate who depends on registration order without saying so has made the test fragile: a refactor can change behavior while all request paths still look familiar. Prefer non-overlapping matchers where possible; use explicit priority when a deliberate fallback is useful.
Say exactly how a request is selected
Walk through one valid call. WireMock sees the method, URL, headers, and body. It evaluates candidate mappings, applies priority, and emits the response from the selected stub. The Content-Type of the response is separate from the JSON predicate on the request. If the client sends the right body to /reservation singular, the /reservations stub should not match. If the client sends GET, a POST stub should not match. That mismatch is useful evidence, not a reason to loosen the test immediately.
A compact Java-style sketch makes the intent visible; exact imports and builder names may vary with WireMock version:
stubFor(post(urlPathEqualTo("/reservations"))
.withHeader("X-Tenant", equalTo("north"))
.withRequestBody(matchingJsonPath("$.orderId", equalTo("123")))
.withRequestBody(matchingJsonPath("$.sku", equalTo("A-1")))
.withRequestBody(matchingJsonPath("$.quantity", equalTo("2")))
.atPriority(1)
.willReturn(aResponse()
.withStatus(202)
.withHeader("Content-Type", "application/json")
.withBody("{\"reservationId\":\"r-7\"}")));
This is a teaching fragment, not a production-complete server setup. For numeric JSON comparison and optional fields, choose a predicate that expresses the actual API contract and test it against representative payloads; string matching on a path-selected JSON value should not be assumed to enforce the entire schema. If the field can be an integer or string, that distinction may itself be worth verifying. Avoid equalToJson on an entire large payload merely to make the test pass; it can reject legitimate extra fields unless configured appropriately. Conversely, a partial matcher will not prove all required fields unless you assert them elsewhere.
When an interview prompt says “the test sometimes chooses the wrong stub,” ask for the registered mappings and a captured request. Draw an overlap table: valid north-tenant request matches the specific stub and the catch-all; invalid quantity should match only the validation stub and perhaps the fallback; missing tenant should never receive the happy-path response. State the priority and expected selection for each row. That is more diagnostic than rearranging stub declarations until the symptom disappears.
Stateful scenarios: a controlled sequence, not hidden global memory
Some behavior genuinely depends on prior calls. Suppose GET /reservations/r-7 first returns PENDING, then later returns ACCEPTED. WireMock's scenario feature lets stubs require a scenario state and change it after a matching request. A scenario begins at STARTED. A first status call can require STARTED, return PENDING, and move the scenario to POLLING; a second can require POLLING, return ACCEPTED, and move to ACCEPTED. Name those states by the external behavior the client is exercising, not by test method names.
The diagram's STARTED to ACCEPTED path is a simplified illustration. A real polling contract may need a middle state, a timeout, or repeated PENDING responses. Write those transitions down before implementing them. Otherwise an accidental extra poll can consume the only pending response and make a flaky test appear to be a timing problem. A deterministic test can control each response and assert that the client stops after acceptance or reacts appropriately to a timeout.
Scenario state is not a substitute for a real backend database or a claim that every request in a suite should share state. It is a deliberate test fixture. If two tests reuse the same named scenario without a reset, the second test may start in ACCEPTED and skip the behavior it was meant to verify. WireMock's JUnit Jupiter extension documents lifecycle controls and default resets for its integration, but the exact setup matters. A manually shared server or customized lifecycle can behave differently. In an interview answer, say where the server is created, what is reset between methods, and how the test proves the initial state rather than assuming it.
Verification checks the client's behavior after the stub responded
A stub returning 202 is not proof that the client made the correct request. A test can pass if it never calls the API but the application returns a hard-coded success. It can also pass if the wrong order ID is sent to a broad stub. WireMock's verification API uses the request journal to assert received calls. Verify the method, path, tenant, and required body fields; assert call count when duplicates are a bug. For a retry test, verify exactly how many calls occurred and under which response sequence, not merely that at least one happened.
For example, after invoking the service, a Java-style verification might say:
verify(1, postRequestedFor(urlPathEqualTo("/reservations"))
.withHeader("X-Tenant", equalTo("north"))
.withRequestBody(matchingJsonPath("$.orderId", equalTo("123"))));
This answers “what did the application send?” It does not prove the warehouse service will accept that body in production. Contract compatibility should be tested against a provider schema or provider test where appropriate; WireMock alone reflects the stub author’s assumptions. Nor does request count alone prove business idempotency. If the app retries a timed-out POST, the real service needs a safe retry contract—perhaps an idempotency key. A mock can expose that the key was sent; it cannot confer idempotency on a server that ignores it.
When verification fails, inspect the nearest received requests and the unmatched-request diagnostics. Compare method, URL encoding, headers, body fields, and JSON types. The failure might be an application bug, a stale stub, or a mistaken test expectation. This is why the request journal is an investigative asset rather than just a final assertion. Preserve diagnostic information on failure, but reset it before the next independent test so an old call cannot satisfy a new assertion.
A reliable test plan for the reservation client
Use a short sequence of tests with independent fixtures. Test one sends a valid reservation and asserts the returned ID plus one matching outbound call. Test two sends quantity zero and expects a handled validation error; the happy-path stub must not answer it. Test three simulates a 503 followed by 202 and verifies a bounded retry policy, provided that retry is actually part of the client contract. Test four exercises PENDING then ACCEPTED polling under a named scenario. Test five starts with the same scenario name in a fresh test and proves the first response is PENDING, demonstrating isolation.
| Failure injection | What the client should do | What WireMock should prove |
|---|---|---|
| Wrong tenant | Avoid claiming a successful north reservation | Happy-path stub does not match |
| Quantity zero | Surface validation without success ID | Validation mapping selected, exact call captured |
Transient 503 | Retry only if policy permits, then settle | Count and order are bounded |
| Extra status poll | Stop or handle final state coherently | Scenario transition is explicit |
| No outgoing request | Fail despite any local success object | Request journal has zero matching calls |
Do not run the sequence as one large test solely to preserve state. Independent tests make failures local. Within a single stateful test, keep the transition order explicit. If tests run in parallel against one shared WireMock server, isolation becomes harder: a reset from one test can erase another's journal or scenario state. Separate servers, unique scenarios, or controlled execution are possible remedies, chosen according to suite cost and runner behavior. A fixed sleep is not a substitute for an observable condition when asynchronous calls are involved.
Interview follow-ups that expose shallow answers
“Why did your stub match?” Name the concrete predicates and the priority rule. “What if two stubs match?” Explain precedence and why overlap is intentional or a defect. “Why use a scenario?” Because the external contract changes based on prior calls, not because the test needs arbitrary mutable state. “How do you prove the client called the dependency?” Verify the request journal after invoking the client. “Why did another test fail only when the suite runs together?” Investigate shared scenario state, registered mappings, request-journal residue, or parallel-server reuse before declaring an application race.
Another useful question is “What should you not mock?” If the behavior under test is your own JSON serialization, HTTP configuration, or retry interceptor, exercise enough real client stack to catch those errors. Mock the external warehouse boundary, not the very method you want to validate. Conversely, a WireMock integration test should not be described as a full end-to-end test with a real warehouse provider. Name the seam and its limitations. This precision is often what separates a plausible interview answer from an operationally trustworthy one.
Sources and Further Reading
- WireMock: Request Matching — request predicates.
- WireMock: Stubbing — mappings and priority.
- WireMock: Stateful Behaviour — scenario states and transitions.
- WireMock: Verifying — request-journal assertions.
- WireMock: JUnit Jupiter Integration — extension lifecycle and resets.