Stateful scenarios let you model multi-step API conversations where MockServer's response changes based on prior interactions. Each scenario is an independent named state machine. Every scenario starts in the "Started" state and advances as expectations match, as a timer fires, or as your test harness pushes it forward via the REST API.

An expectation joins a scenario by setting scenarioName (which machine), scenarioState (the required current state), and newScenarioState (the state to enter after a match). Expectations without a scenarioName match regardless of any scenario state. Scenarios are independent of each other; all scenario states are cleared when MockServer is reset.

Every feature on this page has a runnable example for all supported clients. See examples/ in the repository.

Feature What it does Examples
State-Machine Scenarios Gate expectations on named state; advance the machine on match curl · Java
Sequential / Cycling Responses Return a different response each call from a list: SEQUENTIAL, RANDOM, WEIGHTED, or SWITCH curl · JSON
Timed & Triggered Scenario Flows Auto-transition after a delay, or jump to any state from a test harness on demand curl · Python
Cross-Protocol Scenario Correlation A DNS query, WebSocket connect, gRPC request, or HTTP request can advance a scenario used by any other expectation curl · Go
Scenario REST API & Client Helper Inspect, set, and trigger scenario state directly via REST or the typed client helpers curl
Multi-Turn LLM Conversations Scenarios power scripted multi-turn agent loops for AI testing LLM mocking docs
 

State-Machine Scenarios

Register a set of expectations that share a scenarioName. Each expectation specifies the state it requires (scenarioState) and the state to enter after it matches (newScenarioState). The scenario starts in "Started" and advances as the matching expectation is used. This lets you model realistic multi-step API conversations — authentication flows, order lifecycles, retry sequences, and pagination — without any custom scripting.

Three expectations share the LoginFlow scenario. The POST /login expectation matches only in the Started state, returns a token, and advances the scenario to LoggedIn (with times.remainingTimes: 1 so it fires exactly once). The GET /profile expectations are gated on the current state and return a different response before and after login.

[
  {
    "httpRequest": { "method": "POST", "path": "/login" },
    "httpResponse": { "statusCode": 200, "body": "{\"token\": \"abc123\"}" },
    "scenarioName": "LoginFlow",
    "scenarioState": "Started",
    "newScenarioState": "LoggedIn",
    "times": { "remainingTimes": 1 }
  },
  {
    "httpRequest": { "method": "GET", "path": "/profile" },
    "httpResponse": { "statusCode": 200, "body": "{\"name\": \"Alice\"}" },
    "scenarioName": "LoginFlow",
    "scenarioState": "LoggedIn"
  },
  {
    "httpRequest": { "method": "GET", "path": "/profile" },
    "httpResponse": { "statusCode": 401, "body": "{\"error\": \"Not authenticated\"}" },
    "scenarioName": "LoginFlow",
    "scenarioState": "Started"
  }
]
# Step 1: login returns a token once, advancing Started -> LoggedIn
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "POST", "path": "/login" },
  "httpResponse": { "statusCode": 200, "body": "{\"token\": \"abc123\"}" },
  "scenarioName": "LoginFlow",
  "scenarioState": "Started",
  "newScenarioState": "LoggedIn",
  "times": { "remainingTimes": 1 }
}'

# Step 2: once LoggedIn, GET /profile returns the user
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/profile" },
  "httpResponse": { "statusCode": 200, "body": "{\"name\": \"Alice\"}" },
  "scenarioName": "LoginFlow",
  "scenarioState": "LoggedIn"
}'

# Step 3: before login (Started), GET /profile is unauthorised
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/profile" },
  "httpResponse": { "statusCode": 401, "body": "{\"error\": \"Not authenticated\"}" },
  "scenarioName": "LoginFlow",
  "scenarioState": "Started"
}'
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;
import org.mockserver.matchers.Times;

mockServerClient
    .when(request().withMethod("POST").withPath("/login"), Times.exactly(1))
    .withScenarioName("LoginFlow")
    .withScenarioState("Started")
    .withNewScenarioState("LoggedIn")
    .respond(response().withStatusCode(200).withBody(json("{\"token\":\"abc123\"}")));

mockServerClient
    .when(request().withMethod("GET").withPath("/profile"))
    .withScenarioName("LoginFlow")
    .withScenarioState("LoggedIn")
    .respond(response().withStatusCode(200).withBody(json("{\"name\":\"Alice\"}")));

mockServerClient
    .when(request().withMethod("GET").withPath("/profile"))
    .withScenarioName("LoginFlow")
    .withScenarioState("Started")
    .respond(response().withStatusCode(401).withBody(json("{\"error\":\"Not authenticated\"}")));
var mockServerClient = require('mockserver-client').mockServerClient;
var client = mockServerClient('localhost', 1080);

await client.mockAnyResponse([
    {
        httpRequest: { method: 'POST', path: '/login' },
        httpResponse: { statusCode: 200, body: JSON.stringify({ token: 'abc123' }) },
        scenarioName: 'LoginFlow',
        scenarioState: 'Started',
        newScenarioState: 'LoggedIn',
        times: { remainingTimes: 1, unlimited: false }
    },
    {
        httpRequest: { method: 'GET', path: '/profile' },
        httpResponse: { statusCode: 200, body: JSON.stringify({ name: 'Alice' }) },
        scenarioName: 'LoginFlow',
        scenarioState: 'LoggedIn'
    },
    {
        httpRequest: { method: 'GET', path: '/profile' },
        httpResponse: { statusCode: 401, body: JSON.stringify({ error: 'Not authenticated' }) },
        scenarioName: 'LoginFlow',
        scenarioState: 'Started'
    }
]);
from mockserver import Expectation, HttpRequest, HttpResponse, MockServerClient, Times

client = MockServerClient('localhost', 1080)

client.upsert(
    Expectation(
        http_request=HttpRequest(method='POST', path='/login'),
        http_response=HttpResponse().with_status_code(200).with_body('{"token":"abc123"}'),
        scenario_name='LoginFlow',
        scenario_state='Started',
        new_scenario_state='LoggedIn',
        times=Times(remaining_times=1, unlimited=False),
    ),
    Expectation(
        http_request=HttpRequest(method='GET', path='/profile'),
        http_response=HttpResponse().with_status_code(200).with_body('{"name":"Alice"}'),
        scenario_name='LoginFlow',
        scenario_state='LoggedIn',
    ),
    Expectation(
        http_request=HttpRequest(method='GET', path='/profile'),
        http_response=HttpResponse().with_status_code(401).with_body('{"error":"Not authenticated"}'),
        scenario_name='LoginFlow',
        scenario_state='Started',
    ),
)
require 'mockserver-client'
include MockServer

client = Client.new('localhost', 1080)

client.upsert(
  Expectation.new(
    http_request: HttpRequest.new(method: 'POST', path: '/login'),
    http_response: HttpResponse.new(status_code: 200, body: '{"token":"abc123"}'),
    times: Times.exactly(1),
    scenario_name: 'LoginFlow',
    scenario_state: 'Started',
    new_scenario_state: 'LoggedIn'
  ),
  Expectation.new(
    http_request: HttpRequest.new(method: 'GET', path: '/profile'),
    http_response: HttpResponse.new(status_code: 200, body: '{"name":"Alice"}'),
    scenario_name: 'LoginFlow',
    scenario_state: 'LoggedIn'
  ),
  Expectation.new(
    http_request: HttpRequest.new(method: 'GET', path: '/profile'),
    http_response: HttpResponse.new(status_code: 401, body: '{"error":"Not authenticated"}'),
    scenario_name: 'LoginFlow',
    scenario_state: 'Started'
  )
)
import mockserver "github.com/mock-server/mockserver-monorepo/mockserver-client-go"

client := mockserver.New("localhost", 1080)

login := mockserver.Response().StatusCode(200).JSONBody(`{"token":"abc123"}`).Build()
profileOK := mockserver.Response().StatusCode(200).JSONBody(`{"name":"Alice"}`).Build()
profile401 := mockserver.Response().StatusCode(401).JSONBody(`{"error":"Not authenticated"}`).Build()
loginReq := mockserver.Request().Method("POST").Path("/login").Build()
profileReq := mockserver.Request().Method("GET").Path("/profile").Build()

client.Upsert(
    mockserver.Expectation{
        HttpRequest: &loginReq, HttpResponse: &login,
        ScenarioName: "LoginFlow", ScenarioState: "Started", NewScenarioState: "LoggedIn",
        Times: mockserver.Once(),
    },
    mockserver.Expectation{
        HttpRequest: &profileReq, HttpResponse: &profileOK,
        ScenarioName: "LoginFlow", ScenarioState: "LoggedIn",
    },
    mockserver.Expectation{
        HttpRequest: &profileReq, HttpResponse: &profile401,
        ScenarioName: "LoginFlow", ScenarioState: "Started",
    },
)
using MockServer.Client;
using MockServer.Client.Models;

var client = new MockServerClient("localhost", 1080);

client.When(HttpRequest.Request().WithMethod("POST").WithPath("/login"), Times.Once())
    .WithScenarioName("LoginFlow")
    .WithScenarioState("Started")
    .WithNewScenarioState("LoggedIn")
    .Respond(HttpResponse.Response().WithStatusCode(200).WithBody("{\"token\":\"abc123\"}"));

client.When(HttpRequest.Request().WithMethod("GET").WithPath("/profile"))
    .WithScenarioName("LoginFlow")
    .WithScenarioState("LoggedIn")
    .Respond(HttpResponse.Response().WithStatusCode(200).WithBody("{\"name\":\"Alice\"}"));

client.When(HttpRequest.Request().WithMethod("GET").WithPath("/profile"))
    .WithScenarioName("LoginFlow")
    .WithScenarioState("Started")
    .Respond(HttpResponse.Response().WithStatusCode(401).WithBody("{\"error\":\"Not authenticated\"}"));
use mockserver_client::{ClientBuilder, Expectation, HttpRequest, HttpResponse, Times};

let client = ClientBuilder::new("localhost".to_string(), 1080).build().unwrap();

let expectations = vec![
    Expectation::new(HttpRequest::new().method("POST").path("/login"))
        .scenario_name("LoginFlow")
        .scenario_state("Started")
        .new_scenario_state("LoggedIn")
        .times(Times::once())
        .respond(HttpResponse::new().status_code(200).body(r#"{"token":"abc123"}"#)),
    Expectation::new(HttpRequest::new().method("GET").path("/profile"))
        .scenario_name("LoginFlow")
        .scenario_state("LoggedIn")
        .respond(HttpResponse::new().status_code(200).body(r#"{"name":"Alice"}"#)),
    Expectation::new(HttpRequest::new().method("GET").path("/profile"))
        .scenario_name("LoginFlow")
        .scenario_state("Started")
        .respond(HttpResponse::new().status_code(401).body(r#"{"error":"Not authenticated"}"#)),
];
client.upsert(&expectations).unwrap();
use MockServer\Expectation;
use MockServer\HttpRequest;
use MockServer\HttpResponse;
use MockServer\MockServerClient;
use MockServer\Times;

$client = new MockServerClient('localhost', 1080);

$client->upsertExpectation(
    (new Expectation())
        ->httpRequest(HttpRequest::request()->method('POST')->path('/login'))
        ->scenarioName('LoginFlow')
        ->scenarioState('Started')
        ->newScenarioState('LoggedIn')
        ->times(Times::once())
        ->httpResponse(HttpResponse::response()->statusCode(200)->body('{"token":"abc123"}'))
);
$client->upsertExpectation(
    (new Expectation())
        ->httpRequest(HttpRequest::request()->method('GET')->path('/profile'))
        ->scenarioName('LoginFlow')
        ->scenarioState('LoggedIn')
        ->httpResponse(HttpResponse::response()->statusCode(200)->body('{"name":"Alice"}'))
);
$client->upsertExpectation(
    (new Expectation())
        ->httpRequest(HttpRequest::request()->method('GET')->path('/profile'))
        ->scenarioName('LoginFlow')
        ->scenarioState('Started')
        ->httpResponse(HttpResponse::response()->statusCode(401)->body('{"error":"Not authenticated"}'))
);

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

 

Sequential / Cycling Responses

A single expectation can return multiple different responses by providing an array in httpResponses instead of a single httpResponse. This does not require a named scenario. The responseMode field controls how responses are selected:

  • SEQUENTIAL (default) — responses are returned in order, cycling back to the first after the last
  • RANDOM — a random response is picked from the list for each request
  • WEIGHTED — a response is selected probabilistically using relative weights in responseWeights (index-aligned with httpResponses)
  • SWITCH — serve the first response for the first switchAfter requests, then switch permanently to the next response; stays on the last response once reached

Use httpResponses (plural) with responseMode: "SEQUENTIAL". The first request returns the first response, the second returns the second, and so on. After the last response the cycle restarts. The fourth call below wraps back to the first (200).

{
  "httpRequest": { "path": "/api/status" },
  "httpResponses": [
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" },
    { "statusCode": 503, "body": "{\"status\": \"degraded\"}" },
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" }
  ],
  "responseMode": "SEQUENTIAL"
}
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "path": "/api/status" },
  "httpResponses": [
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" },
    { "statusCode": 503, "body": "{\"status\": \"degraded\"}" },
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" }
  ]
}'
# GET /api/status 4 times -> 200, 503, 200, 200 (4th cycles back to first)
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;
import static org.mockserver.model.JsonBody.json;
import org.mockserver.mock.ResponseMode;
import java.util.Arrays;

mockServerClient
    .when(request().withMethod("GET").withPath("/api/status"))
    .withResponseMode(ResponseMode.SEQUENTIAL)
    .withHttpResponses(Arrays.asList(
        response().withStatusCode(200).withBody(json("{\"status\":\"ok\"}")),
        response().withStatusCode(503).withBody(json("{\"status\":\"degraded\"}")),
        response().withStatusCode(200).withBody(json("{\"status\":\"ok\"}"))
    ));
await client.mockAnyResponse({
    httpRequest: { method: 'GET', path: '/api/status' },
    httpResponses: [
        { statusCode: 200, body: JSON.stringify({ status: 'ok' }) },
        { statusCode: 503, body: JSON.stringify({ status: 'degraded' }) },
        { statusCode: 200, body: JSON.stringify({ status: 'ok' }) }
    ],
    responseMode: 'SEQUENTIAL'
});
from mockserver import Expectation, HttpRequest, HttpResponse, MockServerClient, ResponseMode

client.upsert(
    Expectation(
        http_request=HttpRequest(method='GET', path='/api/status'),
        http_responses=[
            HttpResponse().with_status_code(200).with_body('{"status":"ok"}'),
            HttpResponse().with_status_code(503).with_body('{"status":"degraded"}'),
            HttpResponse().with_status_code(200).with_body('{"status":"ok"}'),
        ],
        response_mode=ResponseMode.SEQUENTIAL,
    )
)
client.upsert(
  Expectation.new(
    http_request: HttpRequest.new(method: 'GET', path: '/api/status'),
    http_responses: [
      HttpResponse.new(status_code: 200, body: '{"status":"ok"}'),
      HttpResponse.new(status_code: 503, body: '{"status":"degraded"}'),
      HttpResponse.new(status_code: 200, body: '{"status":"ok"}')
    ],
    response_mode: ResponseMode::SEQUENTIAL
  )
)
client.
    When(mockserver.Request().Method("GET").Path("/api/status")).
    WithResponseMode(mockserver.ResponseModeSequential).
    RespondMultiple(
        mockserver.Response().StatusCode(200).JSONBody(`{"status":"ok"}`),
        mockserver.Response().StatusCode(503).JSONBody(`{"status":"degraded"}`),
        mockserver.Response().StatusCode(200).JSONBody(`{"status":"ok"}`),
    )
client.When(HttpRequest.Request().WithMethod("GET").WithPath("/api/status"))
    .WithResponseMode(ResponseMode.SEQUENTIAL)
    .Respond(new[]
    {
        HttpResponse.Response().WithStatusCode(200).WithBody("{\"status\":\"ok\"}").Build(),
        HttpResponse.Response().WithStatusCode(503).WithBody("{\"status\":\"degraded\"}").Build(),
        HttpResponse.Response().WithStatusCode(200).WithBody("{\"status\":\"ok\"}").Build()
    });
let expectation = Expectation::new(HttpRequest::new().method("GET").path("/api/status"))
    .http_responses(vec![
        HttpResponse::new().status_code(200).body(r#"{"status":"ok"}"#),
        HttpResponse::new().status_code(503).body(r#"{"status":"degraded"}"#),
        HttpResponse::new().status_code(200).body(r#"{"status":"ok"}"#),
    ])
    .response_mode(ResponseMode::Sequential);
client.upsert(&[expectation]).unwrap();
$client->upsertExpectation(
    (new Expectation())
        ->httpRequest(HttpRequest::request()->method('GET')->path('/api/status'))
        ->responseMode(ResponseMode::SEQUENTIAL)
        ->httpResponses(
            HttpResponse::response()->statusCode(200)->body('{"status":"ok"}'),
            HttpResponse::response()->statusCode(503)->body('{"status":"degraded"}'),
            HttpResponse::response()->statusCode(200)->body('{"status":"ok"}'),
        )
);

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

With responseMode: "SWITCH" and switchAfter: 3, the first three requests return the first response and every subsequent request returns the second. Useful for modelling a service that works for a while and then degrades, without needing a full named scenario.

{
  "httpRequest": { "path": "/api/orders" },
  "httpResponses": [
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" },
    { "statusCode": 503, "body": "{\"status\": \"unavailable\"}" }
  ],
  "responseMode": "SWITCH",
  "switchAfter": 3
}
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "path": "/api/orders" },
  "httpResponses": [
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" },
    { "statusCode": 503, "body": "{\"status\": \"unavailable\"}" }
  ],
  "responseMode": "SWITCH",
  "switchAfter": 3
}'
# Calls 1-3: 200; call 4 onwards: 503

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

With responseMode: "WEIGHTED", each response is selected probabilistically using the relative weights in responseWeights (index-aligned with httpResponses). Weights do not need to sum to 100 — they are normalised internally. A weight of [4, 1] and [80, 20] both produce an 80% / 20% split.

{
  "httpRequest": { "path": "/api/payments" },
  "httpResponses": [
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" },
    { "statusCode": 503, "body": "{\"status\": \"unavailable\"}" }
  ],
  "responseMode": "WEIGHTED",
  "responseWeights": [80, 20]
}
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "path": "/api/payments" },
  "httpResponses": [
    { "statusCode": 200, "body": "{\"status\": \"ok\"}" },
    { "statusCode": 503, "body": "{\"status\": \"unavailable\"}" }
  ],
  "responseMode": "WEIGHTED",
  "responseWeights": [80, 20]
}'

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

 

Timed & Triggered Scenario Flows

Beyond expectation-driven state transitions, MockServer supports timed auto-transitions and external triggers via REST endpoints and typed client helpers. These let you model scenarios where state changes happen after a time delay (for example, a background job completing) or are driven by a test harness rather than by an incoming request.

Register expectations for each state, then call PUT /mockserver/scenario/{name} with state, transitionAfterMs, and nextState. The scenario enters the state immediately and automatically advances after the delay. The transition only fires if the scenario is still in the expected state when the timer expires — any intervening manual trigger or expectation match cancels it.

// First register state-gated expectations:
// { "httpRequest": { "path": "/status" }, "httpResponse": { "body": "deploying" },
//   "scenarioName": "DeployFlow", "scenarioState": "Deploying" }
// { "httpRequest": { "path": "/status" }, "httpResponse": { "body": "complete" },
//   "scenarioName": "DeployFlow", "scenarioState": "Deployed" }

// Then PUT to /mockserver/scenario/DeployFlow:
{
  "state": "Deploying",
  "transitionAfterMs": 1000,
  "nextState": "Deployed"
}
# Register the two expectations (one per state):
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/status" },
  "httpResponse": { "statusCode": 200, "body": "deploying" },
  "scenarioName": "DeployFlow",
  "scenarioState": "Deploying"
}'
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/status" },
  "httpResponse": { "statusCode": 200, "body": "complete" },
  "scenarioName": "DeployFlow",
  "scenarioState": "Deployed"
}'

# Enter "Deploying" now, auto-advance to "Deployed" after 1000ms:
curl -X PUT http://localhost:1080/mockserver/scenario/DeployFlow \
  -H "Content-Type: application/json" \
  -d '{ "state": "Deploying", "transitionAfterMs": 1000, "nextState": "Deployed" }'

# GET /status returns "deploying" now, "complete" after ~1s
// Register expectations for each state first ...

// Then schedule the timed transition:
mockServerClient.scenario("DeployFlow").set("Deploying", 1000L, "Deployed");

// GET /status returns "deploying" immediately, "complete" after ~1s
Thread.sleep(1300L);
// now "complete"
// Register expectations for each state first ...

// Set the starting state with a timed auto-transition:
await client.scenario('DeployFlow').set('Deploying', { transitionAfterMs: 1000, nextState: 'Deployed' });

await new Promise(resolve => setTimeout(resolve, 1300));
// GET /status now returns "complete"
import time

# Register expectations for each state first ...

client.scenario('DeployFlow').set(
    'Deploying', transition_after_ms=1000, next_state='Deployed'
)

time.sleep(1.3)
# GET /status now returns "complete"
# Register expectations for each state first ...

client.scenario('DeployFlow').set('Deploying', transition_after_ms: 1000, next_state: 'Deployed')

sleep(1.3)
# GET /status now returns "complete"
import "time"

// Register expectations for each state first ...

client.Scenario("DeployFlow").SetTimed("Deploying", 1000, "Deployed")

time.Sleep(1300 * time.Millisecond)
// GET /status now returns "complete"
// Register expectations for each state first ...

client.Scenario("DeployFlow").Set("Deploying", 1000, "Deployed");

await Task.Delay(1300);
// GET /status now returns "complete"
use std::time::Duration;

// Register expectations for each state first ...

client.scenario("DeployFlow").set_timed("Deploying", 1000, "Deployed").unwrap();

std::thread::sleep(Duration::from_millis(1300));
// GET /status now returns "complete"
// Register expectations for each state first ...

$client->scenario('DeployFlow')->set('Deploying', 1000, 'Deployed');

usleep(1_300_000);
// GET /status now returns "complete"

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

Call PUT /mockserver/scenario/{name}/trigger with {"newState": "..."} to jump the scenario to any state immediately. This is ideal for simulating failure injection, CI pipeline gates, or test teardown without waiting for a timer.

// PUT to /mockserver/scenario/HealthFlow/trigger:
{ "newState": "Down" }
# Register expectations first:
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/health" },
  "httpResponse": { "statusCode": 200, "body": "healthy" },
  "scenarioName": "HealthFlow",
  "scenarioState": "Started"
}'
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/health" },
  "httpResponse": { "statusCode": 503, "body": "down" },
  "scenarioName": "HealthFlow",
  "scenarioState": "Down"
}'

# GET /health -> 200 healthy (default Started state)

# Trigger the failure from the test harness:
curl -X PUT http://localhost:1080/mockserver/scenario/HealthFlow/trigger \
  -H "Content-Type: application/json" \
  -d '{ "newState": "Down" }'

# GET /health -> 503 down
// Register expectations first ...

mockServerClient.scenario("HealthFlow").trigger("Down");
// Register expectations first ...

await client.scenario('HealthFlow').trigger('Down');
# Register expectations first ...

client.scenario('HealthFlow').trigger('Down')
# Register expectations first ...

client.scenario('HealthFlow').trigger('Down')
// Register expectations first ...

client.Scenario("HealthFlow").Trigger("Down")
// Register expectations first ...

client.Scenario("HealthFlow").Trigger("Down");
// Register expectations first ...

client.scenario("HealthFlow").trigger("Down").unwrap();
// Register expectations first ...

$client->scenario('HealthFlow')->trigger('Down');

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

 

Cross-Protocol Scenario Correlation

Scenario state transitions are not limited to HTTP request matches. A DNS query, WebSocket connection, gRPC request, or HTTP request can advance a named scenario via a crossProtocolScenarios array on an expectation. This lets you model multi-protocol test flows where, for example, a WebSocket connection must be established before certain HTTP endpoints become active.

Each entry in crossProtocolScenarios specifies:

  • trigger — the event type: HTTP_REQUEST, WEBSOCKET_CONNECT, DNS_QUERY, or GRPC_REQUEST
  • matchPattern — optional substring filter on the event identifier (HTTP path, WebSocket URL, DNS query name, or gRPC service name); omit to match all events of that type
  • scenarioName — the scenario to advance
  • targetState — the state to transition to

Multiple entries are evaluated independently — a single event can advance several scenarios at once.

The first expectation handles GET /events and carries a crossProtocolScenarios entry with trigger: "HTTP_REQUEST" that advances ConnFlow to Connected. The second expectation gates on that state, so it only matches after /events has been called. The same mechanism works with WEBSOCKET_CONNECT, DNS_QUERY, and GRPC_REQUEST triggers.

[
  {
    "httpRequest": { "method": "GET", "path": "/events" },
    "httpResponse": { "statusCode": 200, "body": "event-stream" },
    "crossProtocolScenarios": [
      {
        "trigger": "HTTP_REQUEST",
        "matchPattern": "/events",
        "scenarioName": "ConnFlow",
        "targetState": "Connected"
      }
    ]
  },
  {
    "httpRequest": { "method": "GET", "path": "/api/conn-status" },
    "httpResponse": { "statusCode": 200, "body": "connected" },
    "scenarioName": "ConnFlow",
    "scenarioState": "Connected"
  }
]
# Expectation 1: /events fires the cross-protocol trigger
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/events" },
  "httpResponse": { "statusCode": 200, "body": "event-stream" },
  "crossProtocolScenarios": [
    {
      "trigger": "HTTP_REQUEST",
      "matchPattern": "/events",
      "scenarioName": "ConnFlow",
      "targetState": "Connected"
    }
  ]
}'

# Expectation 2: only active once ConnFlow is Connected
curl -X PUT http://localhost:1080/mockserver/expectation -d '{
  "httpRequest": { "method": "GET", "path": "/api/conn-status" },
  "httpResponse": { "statusCode": 200, "body": "connected" },
  "scenarioName": "ConnFlow",
  "scenarioState": "Connected"
}'

# Before /events: GET /api/conn-status -> 404 (unmatched)
# After  /events: GET /api/conn-status -> 200
import org.mockserver.model.CrossProtocolScenario;

mockServerClient
    .when(request().withMethod("GET").withPath("/events"))
    .withCrossProtocolScenario(
        CrossProtocolScenario.onHttpPath("/events", "ConnFlow", "Connected"))
    .respond(response().withStatusCode(200));

mockServerClient
    .when(request().withMethod("GET").withPath("/api/conn-status"))
    .withScenarioName("ConnFlow")
    .withScenarioState("Connected")
    .respond(response().withStatusCode(200).withBody(json("{\"status\":\"connected\"}")));
await client.mockAnyResponse([
    {
        httpRequest: { method: 'GET', path: '/events' },
        httpResponse: { statusCode: 200, body: JSON.stringify({ stream: 'open' }) },
        crossProtocolScenarios: [{
            trigger: 'HTTP_REQUEST',
            matchPattern: '/events',
            scenarioName: 'ConnFlow',
            targetState: 'Connected'
        }]
    },
    {
        httpRequest: { method: 'GET', path: '/api/status' },
        httpResponse: { statusCode: 200, body: JSON.stringify({ status: 'connected' }) },
        scenarioName: 'ConnFlow',
        scenarioState: 'Connected'
    }
]);
from mockserver import CrossProtocolScenario, Expectation, HttpRequest, HttpResponse, MockServerClient

client.upsert(
    Expectation(
        http_request=HttpRequest(method='GET', path='/events'),
        http_response=HttpResponse().with_status_code(200).with_body('{"status":"subscribed"}'),
        cross_protocol_scenarios=[
            CrossProtocolScenario(
                trigger='HTTP_REQUEST',
                match_pattern='/events',
                scenario_name='ConnFlow',
                target_state='Connected',
            )
        ],
    ),
    Expectation(
        http_request=HttpRequest(method='GET', path='/api/conn-status'),
        http_response=HttpResponse().with_status_code(200).with_body('{"status":"connected"}'),
        scenario_name='ConnFlow',
        scenario_state='Connected',
    ),
)
client.upsert(
  Expectation.new(
    http_request: HttpRequest.new(method: 'GET', path: '/events'),
    http_response: HttpResponse.new(status_code: 200),
    cross_protocol_scenarios: [
      CrossProtocolScenario.new(
        trigger: CrossProtocolTrigger::HTTP_REQUEST,
        match_pattern: '/events',
        scenario_name: 'ConnFlow',
        target_state: 'Connected'
      )
    ]
  ),
  Expectation.new(
    http_request: HttpRequest.new(method: 'GET', path: '/api/conn-status'),
    http_response: HttpResponse.new(status_code: 200, body: '{"status":"connected"}'),
    scenario_name: 'ConnFlow',
    scenario_state: 'Connected'
  )
)
client.
    When(mockserver.Request().Method("GET").Path("/events")).
    WithCrossProtocolScenario(mockserver.CrossProtocolScenario{
        Trigger:      mockserver.CrossProtocolTriggerHTTPRequest,
        MatchPattern: "/events",
        ScenarioName: "ConnFlow",
        TargetState:  "Connected",
    }).
    Respond(mockserver.Response().StatusCode(200).JSONBody(`{"status":"listening"}`))

apiStatusReq := mockserver.Request().Method("GET").Path("/api/status").Build()
connected := mockserver.Response().StatusCode(200).JSONBody(`{"status":"connected"}`).Build()
client.Upsert(mockserver.Expectation{
    HttpRequest:   &apiStatusReq,
    HttpResponse:  &connected,
    ScenarioName:  "ConnFlow",
    ScenarioState: "Connected",
})
client.When(HttpRequest.Request().WithMethod("GET").WithPath("/events"))
    .WithCrossProtocolScenario(new CrossProtocolScenario
    {
        Trigger = CrossProtocolTrigger.HTTP_REQUEST,
        MatchPattern = "/events",
        ScenarioName = "ConnFlow",
        TargetState = "Connected"
    })
    .Respond(HttpResponse.Response().WithStatusCode(200).WithBody("{\"events\":\"subscribed\"}"));

client.When(HttpRequest.Request().WithMethod("GET").WithPath("/api/conn-status"))
    .WithScenarioName("ConnFlow")
    .WithScenarioState("Connected")
    .Respond(HttpResponse.Response().WithStatusCode(200).WithBody("{\"status\":\"connected\"}"));
use mockserver_client::{CrossProtocolScenario, CrossProtocolTrigger, Expectation, HttpRequest, HttpResponse};

let expectations = vec![
    Expectation::new(HttpRequest::new().method("GET").path("/events"))
        .cross_protocol_scenario(
            CrossProtocolScenario::new(CrossProtocolTrigger::HttpRequest, "ConnFlow", "Connected")
                .match_pattern("/events"),
        )
        .respond(HttpResponse::new().status_code(200).body(r#"{"events":"subscribed"}"#)),
    Expectation::new(HttpRequest::new().method("GET").path("/api/conn-status"))
        .scenario_name("ConnFlow")
        .scenario_state("Connected")
        .respond(HttpResponse::new().status_code(200).body(r#"{"status":"connected"}"#)),
];
client.upsert(&expectations).unwrap();
use MockServer\CrossProtocolScenario;
use MockServer\CrossProtocolTrigger;

$client->upsertExpectation(
    (new Expectation())
        ->httpRequest(HttpRequest::request()->method('GET')->path('/events'))
        ->httpResponse(HttpResponse::response()->statusCode(200))
        ->crossProtocolScenarios(
            CrossProtocolScenario::trigger(CrossProtocolTrigger::HTTP_REQUEST)
                ->matchPattern('/events')
                ->scenarioName('ConnFlow')
                ->targetState('Connected'),
        )
);
$client->upsertExpectation(
    (new Expectation())
        ->httpRequest(HttpRequest::request()->method('GET')->path('/api/conn-status'))
        ->scenarioName('ConnFlow')
        ->scenarioState('Connected')
        ->httpResponse(HttpResponse::response()->statusCode(200)->body('{"status":"connected"}'))
);

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

 

Scenario REST API & Client Helper

The following endpoints let you inspect and control scenario state directly, without going through expectation matching. All typed clients expose these as a scenario(name) helper.

Method Endpoint Body What it does
GET /mockserver/scenario Return all known scenarios and their current state
GET /mockserver/scenario/{name} Return {"scenarioName": "...", "currentState": "..."} for the named scenario
PUT /mockserver/scenario/{name} {"state": "...", "transitionAfterMs"?: N, "nextState"?: "..."} Set the scenario to state immediately; optionally schedule an auto-transition to nextState after transitionAfterMs milliseconds
PUT /mockserver/scenario/{name}/trigger {"newState": "..."} Jump the scenario to newState immediately, cancelling any pending timed transition
# List all scenarios and their current state:
curl -X GET http://localhost:1080/mockserver/scenario

# Get the current state of a specific scenario:
curl -X GET http://localhost:1080/mockserver/scenario/DeployFlow
# -> {"scenarioName": "DeployFlow", "currentState": "Started"}

# Set state immediately (no timed transition):
curl -X PUT http://localhost:1080/mockserver/scenario/DeployFlow \
  -H "Content-Type: application/json" \
  -d '{"state": "Deploying"}'

# Set state with a timed auto-transition after 5 seconds:
curl -X PUT http://localhost:1080/mockserver/scenario/DeployFlow \
  -H "Content-Type: application/json" \
  -d '{"state": "Deploying", "transitionAfterMs": 5000, "nextState": "Deployed"}'

# Jump to a state immediately (cancels any pending timed transition):
curl -X PUT http://localhost:1080/mockserver/scenario/DeployFlow/trigger \
  -H "Content-Type: application/json" \
  -d '{"newState": "Failed"}'
// Inspect state:
String state = mockServerClient.scenario("DeployFlow").state();

// Set state immediately:
mockServerClient.scenario("DeployFlow").set("Deploying");

// Set state with timed auto-transition (transitionAfterMs, nextState):
mockServerClient.scenario("DeployFlow").set("Deploying", 5000L, "Deployed");

// Jump to a state immediately:
mockServerClient.scenario("DeployFlow").trigger("Failed");

// List all scenarios:
List<ScenarioState> all = mockServerClient.scenarios();
// Get state:
const state = await client.scenario('DeployFlow').state();

// Set state immediately:
await client.scenario('DeployFlow').set('Deploying');

// Set state with timed auto-transition:
await client.scenario('DeployFlow').set('Deploying', { transitionAfterMs: 5000, nextState: 'Deployed' });

// Jump immediately:
await client.scenario('DeployFlow').trigger('Failed');

// List all:
const all = await client.scenarios();
# Get state:
state = client.scenario('DeployFlow').state()

# Set state immediately:
client.scenario('DeployFlow').set('Deploying')

# Set state with timed auto-transition:
client.scenario('DeployFlow').set('Deploying', transition_after_ms=5000, next_state='Deployed')

# Jump immediately:
client.scenario('DeployFlow').trigger('Failed')

# List all:
all_scenarios = client.scenarios()
# Get state:
state = client.scenario('DeployFlow').state

# Set state immediately:
client.scenario('DeployFlow').set('Deploying')

# Set state with timed auto-transition:
client.scenario('DeployFlow').set('Deploying', transition_after_ms: 5000, next_state: 'Deployed')

# Jump immediately:
client.scenario('DeployFlow').trigger('Failed')

# List all:
all = client.scenarios
// Get state:
state, err := client.Scenario("DeployFlow").State()

// Set state immediately:
client.Scenario("DeployFlow").Set("Deploying")

// Set state with timed auto-transition:
client.Scenario("DeployFlow").SetTimed("Deploying", 5000, "Deployed")

// Jump immediately:
client.Scenario("DeployFlow").Trigger("Failed")

// List all:
all, err := client.Scenarios()
// Get state:
var state = client.Scenario("DeployFlow").State();

// Set state immediately:
client.Scenario("DeployFlow").Set("Deploying");

// Set state with timed auto-transition:
client.Scenario("DeployFlow").Set("Deploying", 5000, "Deployed");

// Jump immediately:
client.Scenario("DeployFlow").Trigger("Failed");

// List all:
var all = client.Scenarios();
// Get state:
let state = client.scenario("DeployFlow").state().unwrap();

// Set state immediately:
client.scenario("DeployFlow").set("Deploying").unwrap();

// Set state with timed auto-transition:
client.scenario("DeployFlow").set_timed("Deploying", 5000, "Deployed").unwrap();

// Jump immediately:
client.scenario("DeployFlow").trigger("Failed").unwrap();

// List all:
let all = client.scenarios().unwrap();
// Get state:
$state = $client->scenario('DeployFlow')->state();

// Set state immediately:
$client->scenario('DeployFlow')->set('Deploying');

// Set state with timed auto-transition:
$client->scenario('DeployFlow')->set('Deploying', 5000, 'Deployed');

// Jump immediately:
$client->scenario('DeployFlow')->trigger('Failed');

// List all:
$all = $client->scenarios();

Runnable example: examples/curl/scenario (and the same under examples/<language>/scenario).

 

Multi-Turn LLM Conversations

Scenarios power scripted multi-turn agent loops for AI testing. Each turn advances the scenario state, so you can specify exactly what the mock LLM returns at each step — first call a tool, next return the final answer, and so on. The isolateBy option lets concurrent sessions (identified by a header, query parameter, or cookie) maintain independent state.

See LLM Response Mocking for the full conversation builder API and per-provider examples.