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 matchers —
path("/things")orpath = beEqual("/things") - Header matchers —
containsHeader("X-Request-ID", "abc")checks for a header with an exact value - Content matchers —
bodyContains("value")checks if the raw body string contains a substring;bodyString += contain("value")adds a Kotest matcher directly - Predicate matchers —
bodyMatchesPredicate { it?.name == "foo" }matches against the typed, deserialized request body — see Typed request body for the full API - Call matchers —
successCallMatchermatches if a function called with the body does not throw - Priority —
priority = 10onRequestSpecificationBuildersets theRequestSpecification.priorityof the stub; higher values indicate higher priority. Default is0. 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);