Verification and request journal

Mokksy provides two complementary verification methods that check opposite sides of the stub/request contract.

Verify all stubs were triggered

verifyNoUnmatchedStubs() fails if any registered stub was never matched by an incoming request. Use this to catch stubs you set up but that were never actually called — a sign the code under test took a different path than expected.

1// Fails if any stub has never been matched
2mokksy.verifyNoUnmatchedStubs()
1// Fails if any stub has never been matched
2mokksy.verifyNoUnmatchedStubs();

Note: Be careful when running tests in parallel against a single MokksyServer instance. Some stubs might be unmatched when one test completes. Avoid calling this in @AfterEach/@AfterTest unless each test owns its own server instance.

Verify no unexpected requests arrived

verifyNoUnexpectedRequests() fails if any HTTP request arrived at the server but no stub matched it. These requests are recorded in the RequestJournal and reported together.

1// Fails if any request arrived with no matching stub
2mokksy.verifyNoUnexpectedRequests()
1// Fails if any request arrived with no matching stub
2mokksy.verifyNoUnexpectedRequests();

Always run verifyNoUnexpectedRequests() in @AfterEach to catch requests that arrived but matched no stub. For verifyNoUnmatchedStubs(), the right placement depends on your fixture strategy:

  • Per-test instance (@TestInstance(Lifecycle.PER_METHOD) or a fresh server per test): call both checks in @AfterEach — every stub registered during that test should have been matched before the server is torn down.
  • Shared instance (@TestInstance(Lifecycle.PER_CLASS) or a companion-object server): call verifyNoUnmatchedStubs() in @AfterAll, immediately before shutdown(). Calling it after each individual test would falsely report stubs registered for later tests as unmatched.
 1@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 2class MyTest {
 3
 4  val mokksy = Mokksy(verbose = true)
 5  lateinit var client: HttpClient
 6
 7  @BeforeAll
 8  suspend fun setup() {
 9    mokksy.startSuspend()
10    mokksy.awaitStarted() // port() and baseUrl() are safe after this point
11    client = HttpClient {
12      install(DefaultRequest) {
13        url(mokksy.baseUrl())
14      }
15    }
16  }
17
18  @Test
19  suspend fun testSomething() {
20    mokksy.get {
21      path("/hi")
22    } respondsWith {
23      delay = 100.milliseconds // wait 100ms, then reply
24      body = "Hello"
25    }
26
27    // when
28    val response = client.get("/hi")
29
30    // then
31    response.status shouldBe HttpStatusCode.OK
32    response.bodyAsText() shouldBe "Hello"
33  }
34
35  @AfterEach
36  fun afterEach() {
37    mokksy.verifyNoUnexpectedRequests()
38  }
39
40  @AfterAll
41  suspend fun afterAll() {
42    client.close()
43    mokksy.verifyNoUnmatchedStubs() // shared instance: check once, after all tests ran
44    mokksy.shutdownSuspend()
45  }
46}
 1@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 2class MyTest {
 3
 4    private final Mokksy mokksy = Mokksy.create().start();
 5    private final HttpClient httpClient = HttpClient.newHttpClient();
 6
 7    @Test
 8    void testSomething() throws Exception {
 9        mokksy.get(spec -> spec.path("/hi"))
10            .respondsWith(response -> response
11                .delayMillis(100)
12                .body("Hello"));
13
14        var response = httpClient.send(
15            HttpRequest.newBuilder()
16                .uri(URI.create(mokksy.baseUrl() + "/hi"))
17                .GET()
18                .build(),
19            HttpResponse.BodyHandlers.ofString()
20        );
21
22        assertThat(response.statusCode()).isEqualTo(200);
23        assertThat(response.body()).isEqualTo("Hello");
24    }
25
26    @AfterEach
27    void afterEach() {
28        mokksy.verifyNoUnexpectedRequests();
29    }
30
31    @AfterAll
32    void afterAll() {
33        mokksy.verifyNoUnmatchedStubs();
34        mokksy.shutdown();
35    }
36}

Inspecting unmatched items

Use the find* variants to retrieve the unmatched items directly for custom assertions:

1// List<RecordedRequest> — HTTP requests with no matching stub
2val unmatchedRequests: List<RecordedRequest> = mokksy.findAllUnexpectedRequests()
3
4// List<RecordedRequest> — matched requests, populated only in JournalMode.FULL
5val matchedRequests: List<RecordedRequest> = mokksy.findAllMatchedRequests()
6
7// List<StubHandle> — stubs that were never triggered
8val unmatchedStubs: List<StubHandle> = mokksy.findAllUnmatchedStubs()
1// List<RecordedRequest> - HTTP requests with no matching stub
2var unmatchedRequests = mokksy.findAllUnexpectedRequests();
3
4// List<StubHandle> - stubs that were never triggered
5var unmatchedStubs = mokksy.findAllUnmatchedStubs();

StubHandle and RecordedRequest answer different questions:

  • StubHandle identifies a registered stub. It exposes the optional stub name, matchCount(), and verification helpers such as verifyCalled(). It does not contain HTTP request details.
  • RecordedRequest is the request journal entry. It captures the incoming request method, uri, headers, whether it matched a stub, and bodyAsText when Mokksy can safely read the request body as text.

Use StubHandle when you need to verify that a known stub was called. Use RecordedRequest when you need to inspect what the client actually sent, especially for unexpected requests.

Request journal

Mokksy records incoming requests in a RequestJournal. The recording mode is controlled by JournalMode in ServerConfiguration:

  • JournalMode.NONE - Disables request recording entirely. findAllUnexpectedRequests(), findAllMatchedRequests(), and verifyNoUnexpectedRequests() throw IllegalStateException.
  • JournalMode.LEAN (default) – Records only requests with no matching stub. Lower overhead; sufficient for verifyNoUnexpectedRequests().
  • JournalMode.FULL - Records all incoming requests, both matched and unmatched.
1val mokksy = MokksyServer(
2  configuration = ServerConfiguration(
3    journalMode = JournalMode.FULL,
4  ),
5)

Call resetMatchState() between scenarios to clear stub match state and the journal:

1@AfterTest
2fun afterEach() {
3  mokksy.resetMatchState()
4}
1@AfterEach
2void afterEach() {
3    mokksy.resetMatchState();
4}

Note: Stubs configured with eventuallyRemove = true are permanently removed from the registry on first match and cannot be re-armed by resetMatchState(). Re-register them before the next scenario.