skip to content

In WireMock, how do you make one dive-log endpoint answer PENDING first and SIGNED on the next call?

level: middleimportance: must knowfreq 70%

answer

  1. two stubs, same URL, different moments
  2. one named machine both stubs join
  3. one requires a state, one sets it
  4. the move happens on serving, not matching
  5. start state, then a name you invent

basics

~20 s

Put both WireMock stubs into one scenario with inScenario. The first requires the start state and calls willSetStateTo to move the conversation on. The second requires that new state and answers only once the first has served.

solid answer

~40 s

In WireMock, a **scenario** is a named state machine, and two stubs on the same dive-log URL are told apart by the state each one requires. Register the first with `inScenario("dive signoff")`, `whenScenarioStateIs(Scenario.STARTED)` and `willSetStateTo("instructor notified")`, returning the `PENDING` payload; register the second with the same scenario name and `whenScenarioStateIs("instructor notified")`, returning `SIGNED`. WireMock checks the URL, method and body matchers first and the scenario condition only after they pass, so the first call gets `PENDING` and, because that stub moved the machine on, the second gets `SIGNED`. The transition happens when a stub actually serves — matching alone never advances the state. Leave `willSetStateTo` off the second stub and the conversation stops there, so every later poll keeps returning `SIGNED`, which is usually exactly what a polling test wants.

code

java · 15 lines
java
String pending = """
    {"diveId": "D-4417", "signoffStatus": "PENDING"}""";
String signed = """
    {"diveId": "D-4417", "signoffStatus": "SIGNED"}""";

stubFor(get(urlPathEqualTo("/dive-log/v1/dives/D-4417/signoff"))
    .inScenario("dive signoff")
    .whenScenarioStateIs(Scenario.STARTED)
    .willSetStateTo("instructor notified")
    .willReturn(aResponse().withStatus(200).withBody(pending)));

stubFor(get(urlPathEqualTo("/dive-log/v1/dives/D-4417/signoff"))
    .inScenario("dive signoff")
    .whenScenarioStateIs("instructor notified")
    .willReturn(aResponse().withStatus(200).withBody(signed)));

go deeper

for a junior

Be ready to name the three WireMock calls that build a conversation: inScenario to join, whenScenarioStateIs to require a state, and willSetStateTo to move on. Know that the first state is Started.

for a middle

Explain that WireMock evaluates the ordinary matchers first and the scenario state second, and that only serving a request advances the machine. Be able to write the two-stub PENDING-then-SIGNED pair from memory.

for a senior

Show judgment about how long a conversation should be. Say when a three-state WireMock scenario earns its maintenance cost against simply pointing the client at two stubbed endpoints, and how you keep state names discoverable to the next reader.

for a principal

Own when stateful stubbing is the right tool at all. A conversation encodes a client protocol into the fixture set, so decide where that belongs, who maintains it, and what a body of stateful WireMock fixtures costs when the upstream protocol moves.

## The problem a scenario solves A stub server answers a request by picking a matching stub, and by default that decision has no memory: the same `GET /dive-log/v1/dives/D-4417/signoff` selects the same WireMock stub on the first call and on the hundredth. Real clients often need the opposite. A dive-log client submits a dive, then polls for the instructor's sign-off, and the interesting test is the one where the first poll comes back `PENDING` and a later one comes back `SIGNED`, because that is the path through the client's own waiting logic that never runs otherwise. WireMock's answer is the **scenario**: a named state machine held by the server, against which a stub may declare both a precondition and an effect. ## The three calls | call | product | role | |---|---|---| | `inScenario("dive signoff")` | WireMock | joins this stub to the named machine | | `whenScenarioStateIs("instructor notified")` | WireMock | precondition — serve only in this state | | `willSetStateTo("instructor notified")` | WireMock | effect — move the machine here after serving | The first stub of a conversation requires `Scenario.STARTED`, which in WireMock is a `String` constant holding the text `Started`, the state every scenario begins in. Each later stub requires the state its predecessor set. ## What advances the state, and what does not This is where most WireMock conversations go wrong, so it is worth stating flatly: - In WireMock the state moves **only when a stub declaring `willSetStateTo(...)` actually serves a request**; a stub that matched but lost to another stub moves nothing. - Evaluating matchers does not move it. WireMock checks the URL, method, header and body matchers first and treats the scenario condition as an additional gate, and failing that gate is a non-match, not a transition. - A WireMock stub with no `willSetStateTo(...)` serves and leaves the state where it was, which is exactly how you build a terminal state that answers every later poll. - Nothing walks the machine backwards on its own; a WireMock conversation goes where its stubs send it and then stops. ## Walking the dive-log sign-off conversation 1. Two WireMock stubs are registered for `GET /dive-log/v1/dives/D-4417/signoff`, both with `inScenario("dive signoff")`. 2. The first requires `Scenario.STARTED`, returns the `PENDING` payload, and declares `willSetStateTo("instructor notified")`. 3. The second requires `"instructor notified"` and returns the `SIGNED` payload, with no transition of its own. 4. The client's first poll arrives. Both stubs match on URL and method; only the first passes the WireMock scenario gate, so `PENDING` is returned and the machine moves to `"instructor notified"`. 5. The second poll arrives. Now only the second stub passes the gate, so the client gets `SIGNED` and its waiting loop exits. 6. Any further poll gets `SIGNED` again, because nothing moved the machine on. That last step is a design choice, not an accident. A conversation that ends in a state some stub requires is stable under retries; a conversation that ends in a state nothing requires leaves the next matching request with no stub in that scenario able to serve it. ## Choosing the shape of the conversation - Keep conversations short. Two or three states model a real protocol step; ten states usually mean the fixture has quietly become a second implementation of the upstream service. - Name states after what has happened rather than after their position, because `"instructor notified"` reads well in a failure message and `"state2"` does not. - Make the last state terminal on purpose, so retries and extra polls stay green. - Put one conversation in one WireMock scenario name, and give a second exchange its own name. - Remember that a WireMock scenario is server-side state: it persists across the requests of a test, and across tests, until something moves it. ## The same idea in the neighbouring products Mountebank reaches the same effect positionally rather than by naming states, cycling through the entries of a single stub's `responses` array with one entry consumed per matching request. In MockServer the nearest controls are `Times.exactly(`, `Times.once(` and `TimeToLive`, which cap how many times and for how long an expectation may be used, so a first-then-second answer is expressed as two expectations with bounded usage rather than as a named state.

  • In WireMock, what happens on the third poll if only two states are stubbed?
    The scenario is left in `instructor notified`, because the second stub declared no `willSetStateTo`. In WireMock that stub keeps matching, so every later call gets `SIGNED` — a stable terminal state, and exactly what a poll-until-done test wants. Had the second stub moved the machine to a third state that nothing requires, the third call would find no stub in that scenario.
  • Does a WireMock stub have to change state to take part in a scenario?
    No. In WireMock a stub may call `inScenario(...)` and `whenScenarioStateIs(...)` without ever calling `willSetStateTo(...)`; it then serves whenever the scenario is in that state and leaves the state alone. The terminal step of a dive-log conversation is usually written that way, so repeated polling keeps returning the same answer.
  • How do you stub a conversation whose steps are different endpoints rather than one?
    The same way. In WireMock, `inScenario(...)` names the machine, not the URL, so a `POST /dive-log/v1/dives` stub can call `willSetStateTo("dive accepted")` and a `GET /dive-log/v1/dives/D-4417/signoff` stub can require that state. The scenario models the exchange, while each stub still matches its own URL and method normally.

A scenario is the stub server's memory of where a conversation has got to, the way a ticket counter's display remembers which number it last called. The same request arriving twice is two moments in one exchange, not one question asked twice.

saying these in an interview costs you the question

  • Thinking two stubs cannot share one URL in WireMock
  • Expecting the state to advance when a stub merely matches
  • Putting willSetStateTo on the stub that should serve second
  • Believing WireMock replays stubs in registration order
  • Assuming the scenario rewinds itself after the last state