skip to content

In WireMock, why can two dive-log endpoints sharing one scenario name break each other?

level: seniorimportance: should knowfreq 48%

answer

  1. the name, not the URL, is the scope
  2. one current state per name
  3. a transition anywhere moves it for everyone
  4. passes alone, fails behind another test
  5. read which state each name sits in

basics

~20 s

A WireMock scenario name identifies one shared state machine. Every stub naming it reads and writes the same state, so two unrelated dive-log conversations advance each other. A stub waiting on a state the other side consumed then never serves.

solid answer

~50 s

In WireMock, `inScenario("dive signoff")` does not scope state to a URL — it joins the stub to a machine identified by that **name**, and every stub using that name shares one current state. Put the dive-upload exchange and the sign-off polling exchange both in `"dive signoff"` and they interleave: a `POST /dive-log/v1/dives` stub firing `willSetStateTo("dive accepted")` moves the machine out from under the sign-off stubs, which are still waiting on `Scenario.STARTED`. The symptom is a test that passes alone and fails in a suite, or fails only when the upload runs first, because the request arrives unmatched while its URL and method are plainly correct. The fix is one scenario name per conversation. WireMock's `getAllScenarios` reports each scenario and the state it currently sits in, which is the fastest way to see that two exchanges are sharing one machine.

code

java · 12 lines
java
// two conversations, two scenario names - each advances on its own
stubFor(post(urlPathEqualTo("/dive-log/v1/dives"))
    .inScenario("dive upload")
    .whenScenarioStateIs(Scenario.STARTED)
    .willSetStateTo("dive accepted")
    .willReturn(aResponse().withStatus(202)));

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

go deeper

for a junior

Know that a WireMock scenario is identified by the name passed to inScenario, and that every stub sharing that name shares one state. Give each conversation its own name.

for a middle

Explain the mechanics: a transition on any stub in a WireMock scenario changes the state every other stub in that scenario is compared against, which is why unrelated exchanges must not share a name.

for a senior

Diagnose the order-dependent failure: a test green alone and red behind another, with a stub that registers fine and never serves. Read WireMock's getAllScenarios for the current state before suspecting the client.

for a principal

Own the naming convention across suites. Decide who owns scenario names, how they stay unique when several teams stub the same dive-log API, and whether stateful fixtures should be shared at all given the coupling they introduce.

## What inScenario actually scopes In WireMock, `inScenario("dive signoff")` takes a **name**, and that name is the entire identity of the state machine. It is not scoped to a URL, a method, a stub, a test class or a thread. Every stub anywhere in the server that passes the same string to `inScenario(...)` reads and writes the same current state, and any of them declaring `willSetStateTo(...)` can move it for all the others. That sharing is the feature. It is what lets a WireMock stub for `POST /dive-log/v1/dives` set up the state that a later stub for `GET /dive-log/v1/dives/D-4417/signoff` requires, so a conversation can span endpoints. It is also the trap, because nothing warns you when two exchanges that have nothing to do with each other pick the same name. ## How two conversations collide 1. A dive-upload fixture registers a WireMock stub for `POST /dive-log/v1/dives`, with `inScenario("dive signoff")`, requiring `Scenario.STARTED` and declaring `willSetStateTo("dive accepted")`. 2. A sign-off fixture, written by someone else, registers WireMock stubs for `GET /dive-log/v1/dives/D-4417/signoff` in the same `"dive signoff"` scenario, the first of them requiring `Scenario.STARTED`. 3. Run the sign-off test alone and the machine is in `Started`, so its first stub passes the gate and the test is green. 4. Run the upload test first and it serves, moving the shared machine to `"dive accepted"`. 5. The sign-off test now meets a machine sitting in `"dive accepted"`. Its first stub requires `Started` and is not a candidate, and no sign-off stub requires `"dive accepted"` either. 6. The sign-off request goes unmatched, and the failure reads as a missing payload rather than as a scenario mismatch. ## Why it presents as flake - It is order-dependent: green alone, red behind another test, which is the classic profile teams label flaky and retry. - It is invisible in the stub definition, because in WireMock the URL, method and body matchers are all correct and re-reading them tells you nothing. - It survives re-running the single test, so the usual first move confirms the wrong hypothesis. - It gets worse as suites grow, since a name chosen for one conversation is easy for a third fixture to reuse. - It cannot be fixed by tightening the WireMock URL matcher, because the URL was never what rejected the request. ## Reading where the machine is WireMock's `getAllScenarios` reports each scenario and the state it currently sits in, and that single reading usually ends the investigation: if two exchanges you believed were independent show up under one scenario name, you have found it. For setting up rather than diagnosing, WireMock's admin surface exposes `PUT /__admin/scenarios/{name}/state`, which names the scenario in the path and moves it to the state you ask for. That lets a test start part-way through a long conversation instead of replaying every earlier call to get there, which is worth having as soon as a conversation is more than two states deep. ## Rules that keep conversations apart - One WireMock scenario name per conversation, chosen to describe the exchange rather than the endpoint or the test. - Keep the names in one shared constant, so a second fixture cannot reuse one by accident and a typo becomes a compile error. - Prefer several short conversations to one long machine that many unrelated stubs touch. - If a stub needs no state at all, leave it out of every WireMock scenario rather than parking it in a convenient one. - Make every state a `willSetStateTo(...)` can reach also a state some stub requires, so the machine never lands somewhere nothing answers. - Treat a scenario name as part of the fixture's public surface, because renaming one on a single stub silently splits one conversation into two machines that no longer see each other's transitions. ## The shape of the same risk elsewhere In MockServer the equivalent sequencing is carried by `Times.exactly(`, `Times.once(` and `TimeToLive` on individual expectations, which cap usage and lifetime per expectation, so the coupling shows up as expectations quietly using up their allowance rather than as a shared named machine. Either way the mechanism to keep straight is the same one: whatever carries a conversation forward is named, it is server-side, and two fixtures that name the same thing are one fixture.

  • In WireMock, how do you begin a test part-way through a long conversation?
    Drive the machine directly rather than replaying the earlier calls. WireMock's admin surface exposes `PUT /__admin/scenarios/{name}/state`, which names the scenario in the path and moves it to the state you ask for, so a test can jump straight to `instructor notified` and assert only the last leg of the dive-log exchange.
  • In WireMock, what does a stub that joins a scenario but requires no state do?
    It serves in every state of that scenario. In WireMock, `inScenario(...)` without `whenScenarioStateIs(...)` adds no state condition, so the stub answers throughout the conversation. That is occasionally useful for a background dive-log endpoint that must always reply, but it also means it keeps serving after the exchange has moved on.

saying these in an interview costs you the question

  • Assuming scenario state is scoped per URL or per stub
  • Reusing one scenario name for every stateful stub in a suite
  • Thinking two stubs cannot transition the same machine
  • Blaming order-dependent failures on the client under test
  • Expecting a scenario to return to its start state on its own