Request matching

Match incoming requests with path, header, body, predicate, and call matchers, then resolve conflicts with specificity and priority.

Request specification matchers

Mokksy provides various matcher types to specify conditions for matching incoming HTTP requests:

  • Path matcherspath("/things") or path = beEqual("/things")
  • Header matcherscontainsHeader("X-Request-ID", "abc") checks for a header with an exact value
  • Content matchersbodyContains("value") checks if the raw body string contains a substring; bodyString += contain("value") adds a Kotest matcher directly
  • Predicate matchersbodyMatchesPredicate { it?.name == "foo" } matches against the typed, deserialized request body — see Typed request body for the full API
  • Call matcherssuccessCallMatcher matches if a function called with the body does not throw
  • Prioritypriority = 10 on RequestSpecificationBuilder sets the RequestSpecification.priority of the stub; higher values indicate higher priority. Default is 0. Use negative values (e.g. priority = -1) for catch-all / fallback stubs. Priority is a tiebreaker: it applies only when two stubs match with an equal number of conditions satisfied. For most cases, specificity-based matching (see below) selects the right stub automatically.

Predicate and call matchers are executable code. Mokksy evaluates matchers while scoring an incoming request against registered stubs, and it evaluates every matcher independently rather than short-circuiting after the first mismatch. Keep matcher side effects deterministic and cheap: a matcher can run for requests that eventually match a different stub, and a throwing matcher is logged and counted as a failed matcher for that stub. If you need to assert application state at the moment a matched response is sent, prefer a respondsWith { ... } lambda in Stubbing responses.

Stub specificity

When multiple stubs could match the same request, Mokksy scores each one by counting how many conditions it satisfies, then selects the highest-scoring stub. A stub with two matching conditions beats a stub with one, regardless of registration order.

 1// Generic: matches any POST to /users
 2mokksy.post {
 3  path("/users")
 4} respondsWith {
 5  body = "any user"
 6}
 7
 8// Specific: matches only requests whose body contains "admin" — two conditions
 9mokksy.post {
10  path("/users")
11  bodyContains("admin")
12} respondsWith {
13  body = "admin user"
14}
15
16// Admin request → specific stub wins (score 2 beats score 1)
17val adminResult = client.post("/users") { setBody("admin") }
18adminResult.bodyAsText() shouldBe "admin user"
19
20// Other request → only the generic stub matches
21val genericResult = client.post("/users") { setBody("regular") }
22genericResult.bodyAsText() shouldBe "any user"
 1// Generic: matches any POST to /users
 2mokksy.post(spec -> spec.path("/users"))
 3    .respondsWith(response -> response.body("any user"));
 4
 5// Specific: matches only requests whose body contains "admin"
 6mokksy.post(spec -> spec
 7    .path("/users")
 8    .bodyContains("admin")
 9).respondsWith(response -> response.body("admin user"));
10
11var adminResult = httpClient.send(
12    HttpRequest.newBuilder()
13        .uri(URI.create(mokksy.baseUrl() + "/users"))
14        .POST(HttpRequest.BodyPublishers.ofString("admin"))
15        .build(),
16    HttpResponse.BodyHandlers.ofString()
17);
18assertThat(adminResult.body()).isEqualTo("admin user");
19
20var genericResult = httpClient.send(
21    HttpRequest.newBuilder()
22        .uri(URI.create(mokksy.baseUrl() + "/users"))
23        .POST(HttpRequest.BodyPublishers.ofString("regular"))
24        .build(),
25    HttpResponse.BodyHandlers.ofString()
26);
27assertThat(genericResult.body()).isEqualTo("any user");

Priority example

If multiple stubs match with the same specificity score, the one with the higher priority value wins:

 1// Catch-all stub with low priority (negative value)
 2mokksy.get {
 3  path = contain("/things")
 4  priority = -1
 5} respondsWith {
 6  body = "Generic Thing"
 7}
 8
 9// Specific stub with high priority (positive value)
10mokksy.get {
11  path = beEqual("/things/special")
12  priority = 1
13} respondsWith {
14  body = "Special Thing"
15}
16
17// when
18val generic = client.get("/things/123")
19val special = client.get("/things/special")
20
21// then
22generic.bodyAsText() shouldBe "Generic Thing"
23special.bodyAsText() shouldBe "Special Thing"
 1// Catch-all stub: matches any POST, returns 400
 2mokksy.post(spec -> {
 3    spec.path("/v1/chat/completions");
 4    spec.bodyMatchesPredicate(body -> true);
 5    spec.priority(-1);
 6}).respondsWith(builder -> builder
 7    .body("{\"error\":\"unsupported request\"}")
 8    .status(400));
 9
10// Specific stub: matches only when body contains "gpt-4", returns 200
11mokksy.post(spec -> {
12    spec.path("/v1/chat/completions");
13    spec.bodyContains("gpt-4");
14    spec.priority(1);
15}).respondsWith(builder -> builder
16    .body("{\"model\":\"gpt-4\"}")
17    .status(200));
18
19// Specific request → specific stub wins
20var specific = httpClient.send(
21    HttpRequest.newBuilder()
22        .uri(URI.create(mokksy.baseUrl() + "/v1/chat/completions"))
23        .header("Content-Type", "application/json")
24        .POST(HttpRequest.BodyPublishers.ofString("{\"model\":\"gpt-4\"}"))
25        .build(),
26    HttpResponse.BodyHandlers.ofString()
27);
28assertThat(specific.statusCode()).isEqualTo(200);
29
30// Unmatched request → catch-all fallback kicks in
31var fallback = httpClient.send(
32    HttpRequest.newBuilder()
33        .uri(URI.create(mokksy.baseUrl() + "/v1/chat/completions"))
34        .header("Content-Type", "application/json")
35        .POST(HttpRequest.BodyPublishers.ofString("{\"model\":\"other\"}"))
36        .build(),
37    HttpResponse.BodyHandlers.ofString()
38);
39assertThat(fallback.statusCode()).isEqualTo(400);