# Mokksy > Mokksy and AI-Mocks - mock HTTP APIs with real-world behavior for Java and Kotlin integration tests ## Docs - [Quick Start (5 minutes)](https://mokksy.dev/docs/mokksy/quick-start.md): Quick Start (5 minutes) - [First integration test](https://mokksy.dev/docs/mokksy/first-integration-test.md): First integration test - [Stubbing responses](https://mokksy.dev/docs/mokksy/stubbing.md): Stubbing responses - [Request matching](https://mokksy.dev/docs/mokksy/request-matching.md): Match incoming requests with path, header, body, predicate, and call matchers, then resolve conflicts with specificity and priority. - [Verification and request journal](https://mokksy.dev/docs/mokksy/verification.md): Verification and request journal - [Multipart and file uploads](https://mokksy.dev/docs/mokksy/multipart.md): Multipart and file uploads - [Streaming and SSE](https://mokksy.dev/docs/mokksy/streaming.md): Server-Sent Events (SSE) enable servers to push updates to clients over a single HTTP connection. The provided code demonstrates how to use mokksy to simulate an SSE stream and verify its response in both Kotlin and Java. - [Failure simulation](https://mokksy.dev/docs/mokksy/failure-simulation.md): Failure simulation - [File-based configuration](https://mokksy.dev/docs/mokksy/file-config.md): File-based configuration - [Docker](https://mokksy.dev/docs/mokksy/docker.md): Docker - [Ktor integration](https://mokksy.dev/docs/mokksy/ktor.md): Embed Mokksy directly inside a Ktor application for integration tests, internal API simulation, and authenticated stub routes. - [Anthropic](https://mokksy.dev/docs/ai-mocks/anthropic.md): Anthropic - [OpenAI](https://mokksy.dev/docs/ai-mocks/openai.md): OpenAI - [Gemini](https://mokksy.dev/docs/ai-mocks/gemini.md): Gemini - [Ollama](https://mokksy.dev/docs/ai-mocks/ollama.md): Ollama - [A2A Protocol](https://mokksy.dev/docs/ai-mocks/a2a.md): Agent2Agent (A2A) Protocol - [Spring Boot](https://mokksy.dev/docs/integrations/spring-boot.md): Use Mokksy as a mock HTTP server in Spring Boot integration tests by pointing application properties, RestClient, or WebClient at mokksy.baseUrl(). - [Quarkus](https://mokksy.dev/docs/integrations/quarkus.md): Test Quarkus applications against Mokksy or AI-Mocks by replacing outbound HTTP and AI provider dependencies with deterministic local endpoints. - [LangChain4j](https://mokksy.dev/docs/integrations/langchain4j.md): Use AI-Mocks with LangChain4j to test provider-backed chat and streaming flows without real OpenAI, Anthropic, or Ollama calls. - [Spring AI](https://mokksy.dev/docs/integrations/spring-ai.md): Test Spring AI clients against provider-compatible AI-Mocks servers for deterministic OpenAI and Gemini behavior, including streaming. - [OpenAI Java SDK](https://mokksy.dev/docs/integrations/openai-sdk.md): Use AI-Mocks OpenAI with the official openai-java SDK for deterministic chat, streaming, embeddings, moderation, and error-path integration tests. - [Anthropic Java SDK](https://mokksy.dev/docs/integrations/anthropic-sdk.md): Use AI-Mocks Anthropic with the official Anthropic Java SDK for deterministic Messages API and streaming integration tests. - [Koog](https://mokksy.dev/docs/integrations/koog.md): Test Koog applications against AI-Mocks provider endpoints. This guide uses a verified OpenAI-backed Spring Boot example for chat, streaming, moderation, and failure paths. - [Mokksy vs WireMock](https://mokksy.dev/docs/compare/wiremock.md): Compare Mokksy and WireMock for HTTP integration testing, SSE, chunked streaming, deterministic failures, and Kotlin-first JVM tests. --- # Quick Start (5 minutes) This guide gets you from an empty test to a local HTTP mock server. You will stub one endpoint, call it through a real HTTP client, and verify the response. ## Add the test dependency Add Mokksy to the test classpath. Most JVM projects should use `mokksy-jvm` as a test dependency. ```kotlin dependencies { testImplementation("dev.mokksy:mokksy-jvm:$latestVersion") } ``` ```xml dev.mokksy mokksy-jvm [LATEST_VERSION] test ``` ## Stub and call an HTTP endpoint Start Mokksy before the system under test creates its HTTP client, register the expected stub, then call the endpoint through a real HTTP client. ```kotlin // before SUT starts val mokksy = Mokksy(verbose = true).start() // SUT setup val client = HttpClient { install(DefaultRequest) { url(mokksy.baseUrl()) } } // Given - before test mokksy.get { path("/accounts/42") } respondsWith { body = """{"id":"42","status":"active"}""" httpStatus = HttpStatusCode.OK } // When val response = client.get("/accounts/42") // Then response.status shouldBe HttpStatusCode.OK response.bodyAsText() shouldBe """{"id":"42","status":"active"}""" ``` ```java // before SUT starts var mokksy = Mokksy.create().start(); // Given - before test mokksy.get(spec -> spec.path("/accounts/42")) .respondsWith(response -> response .body("{\"id\":\"42\",\"status\":\"active\"}") .status(200)); // When var httpClient = HttpClient.newHttpClient(); var response = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/accounts/42")) .GET() .build(), HttpResponse.BodyHandlers.ofString() ); // Then assertThat(response.statusCode()).isEqualTo(200); assertThat(response.body()).isEqualTo("{\"id\":\"42\",\"status\":\"active\"}"); // after SUT stop (or never) mokksy.shutdown(); ``` ## What this proves - Your test talks to a real HTTP server. - The external service is replaced by Mokksy. - The response is deterministic and can run in CI without API keys or network access. Next, build a complete [first integration test](../first-integration-test/) or test a [streaming API](../streaming/). # First integration test Use Mokksy when your code normally calls an external HTTP API: payments, customer data, fraud scoring, telecom provisioning, document processing, or internal platform services. ```text Application under test -> Mokksy -> Stubbed external HTTP API ``` ## Test shape 1. Start Mokksy on a random local port. 2. Configure the application under test to use `mokksy.baseUrl()`. 3. Stub the external endpoint and response. 4. Execute the real application behavior. 5. Verify the response and the request journal. ## Example ```kotlin val mokksy = Mokksy(verbose = true).start() val client = HttpClient { install(DefaultRequest) { url(mokksy.baseUrl()) } } mokksy.post { path("/risk/check") bodyContains("customer-123") } respondsWith { httpStatus = HttpStatusCode.Accepted body = """{"decision":"review"}""" } val response = client.post("/risk/check") { contentType(ContentType.Application.Json) setBody("""{"customerId":"customer-123","amount":2500}""") } response.status shouldBe HttpStatusCode.Accepted response.bodyAsText() shouldBe """{"decision":"review"}""" mokksy.verifyNoUnexpectedRequests() mokksy.verifyNoUnmatchedStubs() ``` ```java var mokksy = Mokksy.create().start(); var httpClient = HttpClient.newHttpClient(); try { mokksy.post(spec -> spec .path("/risk/check") .bodyContains("customer-123") ).respondsWith(response -> response .status(202) .body("{\"decision\":\"review\"}")); var response = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/risk/check")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString( "{\"customerId\":\"customer-123\",\"amount\":2500}" )) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(response.statusCode()).isEqualTo(202); assertThat(response.body()).isEqualTo("{\"decision\":\"review\"}"); mokksy.verifyNoUnexpectedRequests(); } finally { mokksy.shutdown(); } ``` This catches two important integration failures: your code sent the wrong request, or it did not call the dependency at all. # Stubbing responses Mokksy supports all HTTP verbs. Here are some examples. ## GET request ```kotlin // given val expectedResponse = // language=json """ { "response": "Pong" } """.trimIndent() mokksy.get { path = beEqual("/ping") containsHeader("Foo", "bar") } respondsWith { body = expectedResponse } // when val result = client.get("/ping") { headers.append("Foo", "bar") } // then result.status shouldBe HttpStatusCode.OK result.bodyAsText() shouldBe expectedResponse ``` ```java // given var expectedResponse = "{\"response\": \"Pong\"}"; mokksy.get(spec -> { spec.path("/ping"); spec.containsHeader("Foo", "bar"); }).respondsWith(builder -> builder.body(expectedResponse)); // when var response = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/ping")) .header("Foo", "bar") .GET() .build(), HttpResponse.BodyHandlers.ofString() ); // then assertThat(response.statusCode()).isEqualTo(200); assertThat(response.body()).isEqualTo(expectedResponse); ``` When the request does not match - Mokksy server returns `404 (Not Found)`: ```kotlin val notFoundResult = client.get("/ping") { headers.append("Foo", "baz") } notFoundResult.status shouldBe HttpStatusCode.NotFound ``` ```java // Request without the required header → 404 var notFound = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/ping")) .header("Foo", "baz") .GET() .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(notFound.statusCode()).isEqualTo(404); ``` ## POST request ```kotlin // given val id = Random.nextInt() val expectedResponse = // language=json """ { "id": "$id", "name": "thing-$id" } """.trimIndent() mokksy.post { path = beEqual("/things") bodyContains("\"$id\"") } respondsWith { body = expectedResponse httpStatus = HttpStatusCode.Created headers { // type-safe builder style append(HttpHeaders.Location, "/things/$id") } headers += "Foo" to "bar" // list style } // when val result = client.post("/things") { headers.append("Content-Type", "application/json") setBody( // language=json """ { "id": "$id" } """.trimIndent(), ) } // then result shouldNotBeNull { status shouldBe HttpStatusCode.Created bodyAsText() shouldBe expectedResponse headers["Location"] shouldBe "/things/$id" headers["Foo"] shouldBe "bar" } ``` ```java // given var expectedBody = "{\"id\":\"42\",\"name\":\"thing-42\"}"; mokksy.post(spec -> { spec.path("/things"); spec.bodyContains("\"42\""); }).respondsWith(builder -> builder .body(expectedBody) .status(201) .header("Location", "/things/42") .header("Foo", "bar")); // when var response = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/things")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"id\":\"42\"}")) .build(), HttpResponse.BodyHandlers.ofString() ); // then assertThat(response.statusCode()).isEqualTo(201); assertThat(response.body()).isEqualTo(expectedBody); assertThat(response.headers().firstValue("Location")).hasValue("/things/42"); assertThat(response.headers().firstValue("Foo")).hasValue("bar"); ``` ## Typed request body When the request body type is known at compile time, use the **reified** overloads to let the compiler infer the type — no explicit `::class` argument required: ```kotlin @Serializable @JvmRecord data class CreateItemRequest(val name: String, val quantity: Int) @Serializable @JvmRecord data class CreateItemResponse(val message: String) ``` ```java record CreateItemRequest(String name, int quantity) {} record CreateItemResponse(String message) {} ``` ### Reified overloads ```kotlin val itemName = "Widget" mokksy.post(name = "create-item") { path("/items") bodyMatchesPredicate("name should match") { it?.name == itemName } } respondsWith { body = CreateItemResponse("Hello, $itemName!") httpStatus = HttpStatusCode.Created headers += "Foo" to "bar" } val result = client.post("/items") { contentType(ContentType.Application.Json) setBody(CreateItemRequest(itemName, quantity = 3)) } result shouldNotBeNull { status shouldBe HttpStatusCode.Created headers["Foo"] shouldBe "bar" body().message shouldBe "Hello, $itemName!" } ``` ```java record CreateItemRequest(String name, int quantity) {} mokksy.post( CreateItemRequest.class, spec -> spec .path("/items") .bodyMatchesPredicate(request -> "widget".equals(request.name())) ).respondsWith(builder -> builder .body("{\"message\":\"Hello, widget!\"}") .status(201) .header("Foo", "bar")); var response = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/items")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"widget\",\"quantity\":3}")) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(response.statusCode()).isEqualTo(201); assertThat(response.body()).isEqualTo("{\"message\":\"Hello, widget!\"}"); assertThat(response.headers().firstValue("Foo")).hasValue("bar"); ``` Reified overloads are provided for all HTTP verbs (`get`, `post`, `put`, `delete`, `patch`, `head`, `options`) and the generic `method` function. Two overloads exist per verb: one taking an optional stub name (`name: String? = null`) and one taking a [`StubConfiguration`](../request-matching/#stub-specificity). The deserialized request body is accessible inside the response lambda as `request.body()`. ### Explicit Class token When the type is determined at runtime or when you want an explicit name on the stub, pass a `kotlin.reflect.KClass` / `java.lang.Class` token using the named `requestType` parameter: ```kotlin mokksy.post(requestType = CreateItemRequest::class) { path("/items/validated") bodyMatchesPredicate("name=widget and quantity>=5") { it?.name == "widget" && (it.quantity) >= 5 } } respondsWith { body = "accepted" httpStatus = HttpStatusCode.Created } val accepted = client.post("/items/validated") { contentType(ContentType.Application.Json) setBody(CreateItemRequest("widget", quantity = 10)) } accepted.status shouldBe HttpStatusCode.Created accepted.bodyAsText() shouldBe "accepted" ``` ```java mokksy.post( CreateItemRequest.class, spec -> spec .path("/items/validated") .bodyMatchesPredicate( "name=widget and quantity>=5", request -> "widget".equals(request.name()) && request.quantity() >= 5 ) ).respondsWith(builder -> builder.body("accepted").status(201)); var accepted = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/items/validated")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"widget\",\"quantity\":10}")) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(accepted.statusCode()).isEqualTo(201); ``` Java supports typed request bodies too. Pass the request class token directly, as shown in the Java tabs above, and use the Kotlin examples as the canonical shape for typed request-body matching. Deserialization uses Ktor's `ContentNegotiation` plugin. For projects that use Jackson instead of `kotlinx.serialization`, create the server with `MokksyJackson.create()` (Java API) — see [Jackson support](#jackson-support) below. When no stub matches and verbose mode is on (`Mokksy(verbose = true)`), Mokksy logs the closest partial match and its failed conditions to help diagnose the mismatch. ### Jackson support By default, Mokksy uses `kotlinx.serialization` for request body deserialization. Java and Kotlin projects that prefer [Jackson](https://github.com/FasterXML/jackson) can configure the server with Ktor's Jackson content negotiation. In the example below `JacksonInput` is deserialized from the request body, and `JacksonOutput` is serialized to response body. ```kotlin val jacksonMokksy = MokksyServer( configuration = ServerConfiguration( verbose = true, contentNegotiationConfigurer = { it.jackson { findAndRegisterModules() } }, ), ).apply { start() } val jacksonClient = HttpClient(Java) { install(ContentNegotiation) { jackson() } install(DefaultRequest) { url(jacksonMokksy.baseUrl()) } } jacksonMokksy .post(requestType = JacksonInput::class) { path = beEqual("/jackson") }.respondsWith(JacksonOutput::class) { val input = request.body() body = JacksonOutput("Hello, ${input.name}") } val result = jacksonClient.post("/jackson") { contentType(ContentType.Application.Json) setBody(JacksonInput("Bob")) } result.status shouldBe HttpStatusCode.OK result.bodyAsText() shouldBe """{"pikka-hi":"Hello, Bob"}""" jacksonMokksy.verifyNoUnexpectedRequests() ``` For Java-first projects that prefer Jackson, use `MokksyJackson.create()`: ```java import dev.mokksy.MokksyJackson; // Default Jackson ObjectMapper Mokksy mokksy = MokksyJackson.create(); mokksy.start(); ``` To customize the `ObjectMapper`, pass a configuration lambda: ```java import com.fasterxml.jackson.databind.ObjectMapper; import dev.mokksy.MokksyJackson; Mokksy mokksy = MokksyJackson.create(ObjectMapper::findAndRegisterModules); mokksy.start(); ``` ## Status-only responses Use `respondsWithStatus` when the test only needs to verify a status code — no body needed. It's an infix function, so it reads naturally next to the stub definition: ```kotlin mokksy.get { path("/ping") } respondsWithStatus HttpStatusCode.NoContent val response = client.get("/ping") response.status shouldBe HttpStatusCode.NoContent ``` ```java mokksy.get(spec -> spec.path("/status-only")) .respondsWithStatus(204); ``` ## One-time stubs Use `StubConfiguration(eventuallyRemove = true)` when a stub should match exactly once and then become ineligible for future requests. This is the supported property for once-only behavior. ```kotlin mokksy.get( configuration = StubConfiguration( name = "single-use", eventuallyRemove = true, ), ) { path("/once") } respondsWith { body = "First and only response" } client.get("/once").status shouldBe HttpStatusCode.OK client.get("/once").status shouldBe HttpStatusCode.NotFound ``` ```java mokksy.get(StubConfiguration.once("single-use"), spec -> spec.path("/once")) .respondsWith(response -> response.body("First and only response")); var first = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/once")) .GET() .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(first.statusCode()).isEqualTo(200); var second = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/once")) .GET() .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(second.statusCode()).isEqualTo(404); ``` ## Run code when a request is matched `respondsWith { ... }` and `respondsWithStream { ... }` are lambdas. Mokksy evaluates the lambda for the matched request immediately before it builds the response, so you can inspect test state, coordinate concurrent code, or build a response from the incoming request. Use this for assertions that must happen at the moment the dependency is called. In this example, the response lambda checks a coroutine `Semaphore` before returning the response: ```kotlin val semaphore = Semaphore(permits = 0) mokksy.post { path("/jobs") } respondsWith { semaphore.availablePermits shouldBe 0 body = "accepted" httpStatus = HttpStatusCode.Accepted semaphore.release() } val response = client.post("/jobs") response.status shouldBe HttpStatusCode.Accepted response.bodyAsText() shouldBe "accepted" semaphore.availablePermits shouldBe 1 ``` ```java var semaphore = new Semaphore(0); mokksy.post(spec -> spec.path("/jobs")) .respondsWith(response -> { assertThat(semaphore.availablePermits()).isZero(); response.status(202).body("accepted"); semaphore.release(); }); var result = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/jobs")) .POST(HttpRequest.BodyPublishers.noBody()) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(result.statusCode()).isEqualTo(202); assertThat(result.body()).isEqualTo("accepted"); assertThat(semaphore.availablePermits()).isEqualTo(1); ``` # Request matching ## Request specification matchers Mokksy provides various matcher types to specify conditions for matching incoming HTTP requests: - **Path matchers** — `path("/things")` or `path = 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](https://kotest.io/docs/assertions/assertions.html) matcher directly - **Predicate matchers** — `bodyMatchesPredicate { it?.name == "foo" }` matches against the typed, deserialized request body — see [Typed request body](../stubbing/#typed-request-body) for the full API - **Call matchers** — `successCallMatcher` matches if a function called with the body does not throw - **Priority** — `priority = 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](../stubbing/#run-code-when-a-request-is-matched). ## 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. ```kotlin // Generic: matches any POST to /users mokksy.post { path("/users") } respondsWith { body = "any user" } // Specific: matches only requests whose body contains "admin" — two conditions mokksy.post { path("/users") bodyContains("admin") } respondsWith { body = "admin user" } // Admin request → specific stub wins (score 2 beats score 1) val adminResult = client.post("/users") { setBody("admin") } adminResult.bodyAsText() shouldBe "admin user" // Other request → only the generic stub matches val genericResult = client.post("/users") { setBody("regular") } genericResult.bodyAsText() shouldBe "any user" ``` ```java // Generic: matches any POST to /users mokksy.post(spec -> spec.path("/users")) .respondsWith(response -> response.body("any user")); // Specific: matches only requests whose body contains "admin" mokksy.post(spec -> spec .path("/users") .bodyContains("admin") ).respondsWith(response -> response.body("admin user")); var adminResult = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/users")) .POST(HttpRequest.BodyPublishers.ofString("admin")) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(adminResult.body()).isEqualTo("admin user"); var genericResult = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/users")) .POST(HttpRequest.BodyPublishers.ofString("regular")) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(genericResult.body()).isEqualTo("any user"); ``` ## Priority example If multiple stubs match with the same specificity score, the one with the higher `priority` value wins: ```kotlin // Catch-all stub with low priority (negative value) mokksy.get { path = contain("/things") priority = -1 } respondsWith { body = "Generic Thing" } // Specific stub with high priority (positive value) mokksy.get { path = beEqual("/things/special") priority = 1 } respondsWith { body = "Special Thing" } // when val generic = client.get("/things/123") val special = client.get("/things/special") // then generic.bodyAsText() shouldBe "Generic Thing" special.bodyAsText() shouldBe "Special Thing" ``` ```java // Catch-all stub: matches any POST, returns 400 mokksy.post(spec -> { spec.path("/v1/chat/completions"); spec.bodyMatchesPredicate(body -> true); spec.priority(-1); }).respondsWith(builder -> builder .body("{\"error\":\"unsupported request\"}") .status(400)); // Specific stub: matches only when body contains "gpt-4", returns 200 mokksy.post(spec -> { spec.path("/v1/chat/completions"); spec.bodyContains("gpt-4"); spec.priority(1); }).respondsWith(builder -> builder .body("{\"model\":\"gpt-4\"}") .status(200)); // Specific request → specific stub wins var specific = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/v1/chat/completions")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"model\":\"gpt-4\"}")) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(specific.statusCode()).isEqualTo(200); // Unmatched request → catch-all fallback kicks in var fallback = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/v1/chat/completions")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString("{\"model\":\"other\"}")) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(fallback.statusCode()).isEqualTo(400); ``` # 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. ```kotlin // Fails if any stub has never been matched mokksy.verifyNoUnmatchedStubs() ``` ```java // Fails if any stub has never been matched mokksy.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. ```kotlin // Fails if any request arrived with no matching stub mokksy.verifyNoUnexpectedRequests() ``` ```java // Fails if any request arrived with no matching stub mokksy.verifyNoUnexpectedRequests(); ``` ## Recommended AfterEach setup 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. ```kotlin @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTest { val mokksy = Mokksy(verbose = true) lateinit var client: HttpClient @BeforeAll suspend fun setup() { mokksy.startSuspend() mokksy.awaitStarted() // port() and baseUrl() are safe after this point client = HttpClient { install(DefaultRequest) { url(mokksy.baseUrl()) } } } @Test suspend fun testSomething() { mokksy.get { path("/hi") } respondsWith { delay = 100.milliseconds // wait 100ms, then reply body = "Hello" } // when val response = client.get("/hi") // then response.status shouldBe HttpStatusCode.OK response.bodyAsText() shouldBe "Hello" } @AfterEach fun afterEach() { mokksy.verifyNoUnexpectedRequests() } @AfterAll suspend fun afterAll() { client.close() mokksy.verifyNoUnmatchedStubs() // shared instance: check once, after all tests ran mokksy.shutdownSuspend() } } ``` ```java @TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTest { private final Mokksy mokksy = Mokksy.create().start(); private final HttpClient httpClient = HttpClient.newHttpClient(); @Test void testSomething() throws Exception { mokksy.get(spec -> spec.path("/hi")) .respondsWith(response -> response .delayMillis(100) .body("Hello")); var response = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/hi")) .GET() .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(response.statusCode()).isEqualTo(200); assertThat(response.body()).isEqualTo("Hello"); } @AfterEach void afterEach() { mokksy.verifyNoUnexpectedRequests(); } @AfterAll void afterAll() { mokksy.verifyNoUnmatchedStubs(); mokksy.shutdown(); } } ``` ## Inspecting unmatched items Use the `find*` variants to retrieve the unmatched items directly for custom assertions: ```kotlin // List — HTTP requests with no matching stub val unmatchedRequests: List = mokksy.findAllUnexpectedRequests() // List — matched requests, populated only in JournalMode.FULL val matchedRequests: List = mokksy.findAllMatchedRequests() // List — stubs that were never triggered val unmatchedStubs: List = mokksy.findAllUnmatchedStubs() ``` ```java // List - HTTP requests with no matching stub var unmatchedRequests = mokksy.findAllUnexpectedRequests(); // List - stubs that were never triggered var 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. ```kotlin val mokksy = MokksyServer( configuration = ServerConfiguration( journalMode = JournalMode.FULL, ), ) ``` Call `resetMatchState()` between scenarios to clear stub match state and the journal: ```kotlin @AfterTest fun afterEach() { mokksy.resetMatchState() } ``` ```java @AfterEach void afterEach() { mokksy.resetMatchState(); } ``` > **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. # Multipart and file uploads This page covers Mokksy's multipart body-matching APIs: `body { form { ... } }`, file-part matchers, `FormEncoding`, and `multipart(...)` for non-form bodies. Use these matchers when your client sends more than JSON. File uploads, mixed metadata-plus-binary requests, and strict form-encoding checks all work through the same request DSL. ## Match multipart form fields and file uploads `body { form { ... } }` matches both `application/x-www-form-urlencoded` and `multipart/form-data` by default. Add `field(...)` matchers for text parts and `file(...)` matchers for uploaded files. ```kotlin mokksy.post { path("/upload") body { form { field("description", "Mokksy upload") file("avatar") { filename("photo.bin") contentType("application/octet-stream") bytes { it?.contentEquals(uploadFile.readBytes()) == true } } } } } respondsWith { body = "file-upload-ok" } val response = client.post("/upload") { setBody( MultiPartFormDataContent( formData { append("description", "Mokksy upload") append( "avatar", uploadFile.readBytes(), Headers.build { append( HttpHeaders.ContentDisposition, "form-data; name=\"avatar\"; filename=\"photo.bin\"", ) append(HttpHeaders.ContentType, "application/octet-stream") }, ) }, ), ) } response.status shouldBe HttpStatusCode.OK response.bodyAsText() shouldBe "file-upload-ok" ``` ```java var uploadFile = Files.createTempFile("avatar", ".bin"); Files.writeString(uploadFile, "expected"); mokksy.post(spec -> spec .path("/upload") .body(body -> body.form(form -> form .field("description", "Mokksy upload") .file("avatar", file -> file .filename("photo.bin") .contentType("application/octet-stream") .bytesMatches(bytes -> { try { org.assertj.core.api.Assertions.assertThat(bytes) .containsExactly(Files.readAllBytes(uploadFile)); return true; } catch (IOException e) { throw new UncheckedIOException(e); } }) ) )) ).respondsWith(rb -> rb.body("file-upload-ok")); ``` For file parts, Mokksy supports `filename(...)`, `contentType(...)`, `text(...)`, and `bytes(...)`. Each matcher adds specificity, so a stub that checks field values and file content automatically outranks a looser fallback stub. ## Restrict accepted form encoding Use `FormEncoding.MULTIPART` or `FormEncoding.URL_ENCODED` when a stub must reject the other form style. Leave the default `AUTO` when either encoding is acceptable. ```kotlin mokksy.post { path("/multipart-only") body { form(FormEncoding.MULTIPART) { field("key", "value") } } } respondsWith { body = "multipart-only-ok" } val multipartResult = client.post("/multipart-only") { setBody( MultiPartFormDataContent( formData { append("key", "value") }, ), ) } val urlEncodedResult = client.submitForm( url = "/multipart-only", formParameters = parameters { append("key", "value") }, ) multipartResult.status shouldBe HttpStatusCode.OK urlEncodedResult.status shouldBe HttpStatusCode.NotFound ``` ```java mokksy.post(spec -> spec .path("/multipart-only") .body(body -> body.form(FormEncoding.MULTIPART, form -> form .field("key", "value") )) ).respondsWith(rb -> rb.body("multipart-only-ok")); ``` ## Match non-form multipart bodies Not every multipart request is `multipart/form-data`. Use `body { multipart(...) { ... } }` for payloads such as `multipart/mixed`, where each part has its own content type and semantic role. ```kotlin mokksy.post { path("/multipart-mixed") body { multipart("multipart/mixed") { boundary("WebAppBoundary") part("metadata") { contentType("application/json") text { it?.contains("Ktor logo") == true } } part("image") { contentType("image/png") bytes { it?.isNotEmpty() == true } } } } } respondsWith { body = "multipart-mixed-ok" } val response = client.post("/multipart-mixed") { setBody( MultiPartFormDataContent( formData { append( "metadata", """{"description":"Ktor logo"}""".encodeToByteArray(), Headers.build { append(HttpHeaders.ContentDisposition, "form-data; name=\"metadata\"") append(HttpHeaders.ContentType, "application/json") }, ) append( "image", "png-data".encodeToByteArray(), Headers.build { append(HttpHeaders.ContentDisposition, "form-data; name=\"image\"") append(HttpHeaders.ContentType, "image/png") }, ) }, boundary = "WebAppBoundary", contentType = ContentType.MultiPart.Mixed.withParameter( "boundary", "WebAppBoundary", ), ), ) } response.status shouldBe HttpStatusCode.OK response.bodyAsText() shouldBe "multipart-mixed-ok" ``` ```java mokksy.post(spec -> spec .path("/multipart-mixed") .body(body -> body.multipart("multipart/mixed", multipart -> multipart .boundary("WebAppBoundary") .part("metadata", part -> part .contentType("application/json") .textMatches(text -> text != null && text.contains("Ktor logo")) ) .part("image", part -> part .contentType("image/png") .bytesMatches(bytes -> bytes != null && bytes.length > 0) ) )) ).respondsWith(rb -> rb.body("multipart-mixed-ok")); ``` # Streaming and SSE ## Server-Sent Events (SSE) [Server-Sent Events (SSE)][sse] allow a server to push updates to the client over a single, long-lived HTTP connection. Streaming clients fail in ways static JSON tests cannot catch: missed chunks, early completion, timeout handling, buffering, and reconnect logic. ```text Client opens SSE connection -> Mokksy sends event chunks -> Client handles stream completion, delay, or timeout ``` This example defines an SSE endpoint, emits two events with controlled timing, and verifies that the client receives a real `text/event-stream` response. ```kotlin mokksy.post { path = beEqual("/sse") } respondsWithSseStream { flow = flow { delay(200.milliseconds) emit( ServerSentEvent( data = "One", ), ) delay(50.milliseconds) emit( ServerSentEvent( data = "Two", ), ) } } // when val result = client.post("/sse") // then result shouldNotBeNull { status shouldBe HttpStatusCode.OK contentType() shouldBe ContentType.Text.EventStream.withCharsetIfNeeded(Charsets.UTF_8) bodyAsText() shouldBe "data: One\r\n\r\ndata: Two\r\n\r\n" } ``` ```java mokksy.post(spec -> spec.path("/sse")) .respondsWithSseStream(builder -> builder .chunk(SseEvent.data("One")) .chunk(SseEvent.data("Two"))); var response = httpClient.send( HttpRequest.newBuilder() .uri(URI.create(mokksy.baseUrl() + "/sse")) .POST(HttpRequest.BodyPublishers.noBody()) .build(), HttpResponse.BodyHandlers.ofString() ); assertThat(response.statusCode()).isEqualTo(200); assertThat(response.body()).isEqualTo("data: One\r\n\r\ndata: Two\r\n\r\n"); ``` ## Long-lived SSE streams By default, the SSE stream closes when the flow completes. To keep it open (e.g. for clients that reconnect on close), end the flow with `awaitCancellation()`: ```kotlin mokksy.post { path = beEqual("/sse-ll") } respondsWithSseStream { flow = flow { emit(ServerSentEvent(data = "hello")) awaitCancellation() } } ``` ```java mokksy.post(spec -> spec.path("/sse-ll")) .respondsWithSseStream(stream -> stream .chunks(Stream.generate(() -> SseEvent.data("heartbeat"))) .delayBetweenChunksMillis(1_000)); ``` ## SSE response with chunk delays Use `delayBetweenChunks` when you want to verify that a client handles events as they arrive, instead of waiting for the full response body. ```kotlin mokksy.get { path("/events") } respondsWithSseStream { delayBetweenChunks = 100.milliseconds chunks += ServerSentEvent(data = """{"status":"accepted"}""") chunks += ServerSentEvent(data = """{"status":"processed"}""") } ``` ```java mokksy.get(spec -> spec.path("/events")) .respondsWithSseStream(stream -> stream .delayBetweenChunksMillis(100) .chunk(SseEvent.data("{\"status\":\"accepted\"}")) .chunk(SseEvent.data("{\"status\":\"processed\"}"))); ``` ## Plain text stream Use `respondsWithStream` for non-SSE streaming responses, such as line-delimited downloads or APIs that return partial data before the transfer completes. ```kotlin mokksy.get { path("/download") } respondsWithStream { delayBetweenChunks = 50.milliseconds chunks += "part-1\n" chunks += "part-2\n" } ``` ```java mokksy.get(spec -> spec.path("/download")) .respondsWithStream(stream -> stream .delayBetweenChunksMillis(50) .chunk("part-1\n") .chunk("part-2\n")); ``` Use these examples to test code that processes data before the full response is available. For timeout, retry, and malformed-stream cases, continue with [Failure simulation](../failure-simulation/). [sse]: https://html.spec.whatwg.org/multipage/server-sent-events.html "Server-Side Events Specification" # Failure simulation Production HTTP clients need more than happy-path JSON. Use these patterns to verify retries, backoff, timeouts, stream parsing, and fallback behavior. ## Delayed response Use `delay` when the server should accept the request but wait before sending the response. ```kotlin mokksy.get { path("/slow") } respondsWith { delay = 2.seconds body = "eventually-ok" } ``` ```java mokksy.get(spec -> spec.path("/slow")) .respondsWith(response -> response .delayMillis(2_000) .body("eventually-ok")); ``` ## Delayed chunks Use `delayBetweenChunks` when the response should arrive incrementally and the client must process partial data. ```kotlin mokksy.get { path("/slow-stream") } respondsWithStream { delayBetweenChunks = 500.milliseconds chunks += "first\n" chunks += "second\n" } ``` ```java mokksy.get(spec -> spec.path("/slow-stream")) .respondsWithStream(stream -> stream .delayBetweenChunksMillis(500) .chunk("first\n") .chunk("second\n")); ``` ## Hanging request or stream Use a stream that never completes when you need to verify client-side read timeouts, cancellation, or reconnect behavior. ```kotlin mokksy.get { path("/never-finishes") } respondsWithSseStream { flow = flow { emit(ServerSentEvent(data = "started")) awaitCancellation() } } ``` ```java mokksy.get(spec -> spec.path("/never-finishes")) .respondsWithSseStream(stream -> stream .chunks(Stream.generate(() -> SseEvent.data("heartbeat"))) .delayBetweenChunksMillis(1_000)); ``` Use this with a short client timeout to verify timeout handling. ## Retry-after and rate limiting Return `429 Too Many Requests` with `Retry-After` when the client should back off and retry later. ```kotlin mokksy.post { path("/payments") } respondsWith { httpStatus = HttpStatusCode.TooManyRequests headers += HttpHeaders.RetryAfter to "30" body = """{"error":"rate_limited"}""" } ``` ```java mokksy.post(spec -> spec.path("/payments")) .respondsWith(response -> response .status(429) .header("Retry-After", "30") .body("{\"error\":\"rate_limited\"}")); ``` ## Malformed SSE Send malformed event-stream data when you need to test parser failures and fallback behavior. ```kotlin mokksy.get { path("/malformed-events") } respondsWithStream { contentType = ContentType.Text.EventStream chunks += "data: valid\n\n" chunks += "this is not a valid event frame" } ``` ```java mokksy.get(spec -> spec.path("/malformed-events")) .respondsWithStream(stream -> stream .contentType("text/event-stream") .chunk("data: valid\n\n") .chunk("this is not a valid event frame")); ``` ## Partial failure Send only part of the expected response when the client must handle incomplete transfers or timeout after partial data. ```kotlin mokksy.get { path("/statement") } respondsWithStream { chunks += "header\n" chunks += "row-1\n" delayBetweenChunks = 250.milliseconds } ``` ```java mokksy.get(spec -> spec.path("/statement")) .respondsWithStream(stream -> stream .chunk("header\n") .chunk("row-1\n") .delayBetweenChunksMillis(250)); ``` Keep the client timeout lower than the full expected transfer time to verify partial-data handling. # File-based configuration File-based configuration lets you define stubs in a YAML file and load them at startup — without writing any Kotlin code. ## Minimal example ```yaml stubs: - name: ping method: GET path: /ping response: body: '{"response":"Pong"}' status: 200 ``` Load the file and start the server: ```kotlin val mokksy = Mokksy().start() mokksy.loadStubsFromFile("/path/to/stubs.yaml") ``` ```java Mokksy mokksy = Mokksy.create().start(); mokksy.loadStubsFromFile("/path/to/stubs.yaml"); ``` ## Stub fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `name` | no | — | Optional identifier shown in logs and error messages | | `method` | no | `GET` | HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` | | `path` | **yes** | — | Exact request path to match | | `match` | no | — | Additional matching criteria (see below) | | `response` | **yes** | — | Response definition | ### Matching criteria ```yaml match: bodyContains: - '"userId":"42"' # request body must contain this string - '"type":"order"' # multiple strings — all must match headers: Authorization: Bearer token123 # request must carry this header value Content-Type: application/json ``` ### Response fields | Field | Required | Default | Description | |-------|----------|---------|-------------| | `type` | no | `plain` | `plain`, `sse`, or `stream` | | `body` | no | — | Response body (`plain` type only) | | `status` | no | `200` | HTTP status code | | `headers` | no | — | Response headers as a map | | `delayMs` | no | `0` | Delay before the response is sent (milliseconds) | | `chunks` | yes\* | — | Ordered list of chunks (`sse` and `stream` types) | | `delayBetweenChunksMs` | no | `0` | Delay between chunks (milliseconds) | | `contentType` | no | — | Override content type for `stream` responses | \* Required when `type` is `sse` or `stream`. ## Response types ### Plain response ```yaml stubs: - name: create-order method: POST path: /orders match: bodyContains: - '"product":"widget"' response: body: '{"orderId":"abc-123"}' status: 201 headers: Location: /orders/abc-123 delayMs: 50 ``` ### Server-Sent Events (SSE) ```yaml stubs: - name: order-updates method: POST path: /orders/stream response: type: sse chunks: - '{"status":"processing"}' - '{"status":"shipped"}' - '{"status":"delivered"}' delayBetweenChunksMs: 100 ``` The response content type is automatically set to `text/event-stream; charset=UTF-8`. ### Plain text stream ```yaml stubs: - name: data-feed method: GET path: /feed response: type: stream chunks: - "chunk-one\n" - "chunk-two\n" - "chunk-three\n" contentType: text/plain; charset=UTF-8 delayBetweenChunksMs: 50 ``` ## Loading the config ### Explicit path ```kotlin val mokksy = Mokksy().start() mokksy.loadStubsFromFile("/app/stubs.yaml") // absolute path mokksy.loadStubsFromFile("stubs.yaml") // relative to working directory ``` ```java Mokksy mokksy = Mokksy.create().start(); mokksy.loadStubsFromFile("/app/stubs.yaml"); // absolute path mokksy.loadStubsFromFile("stubs.yaml"); // relative to working directory ``` ### Environment variable or system property `loadStubsFromEnv()` checks `MOKKSY_CONFIG` first, then the `-Dmokksy.config` system property. When either is set, `start()` loads the stubs automatically — you do not need to call `loadStubsFromEnv()` explicitly in that case. ```kotlin // explicit load — useful when MOKKSY_CONFIG is not in the environment val mokksy = Mokksy().start() mokksy.loadStubsFromEnv() ``` ```java // explicit load — useful when MOKKSY_CONFIG is not in the environment Mokksy mokksy = Mokksy.create().start(); mokksy.loadStubsFromEnv(); ``` ```bash # via environment variable — stubs are loaded automatically on start() MOKKSY_CONFIG=/app/stubs.yaml java -jar app.jar # via system property java -Dmokksy.config=/app/stubs.yaml -jar app.jar ``` ## Validation errors Mokksy validates the config at load time and reports clear errors when something is wrong: | Problem | Error message | |---------|---------------| | File not found | `Mokksy config file not found: /path/to/stubs.yaml` | | Malformed YAML | `Invalid YAML in Mokksy config file '/path/...': ` | | Unknown HTTP method | `: unknown HTTP method 'BREW'. Valid methods: GET, POST, ...` | | Stream with no chunks | `: response type 'sse' requires at least one chunk` | ## Combining file config with code stubs File-based configuration and the code API can be used together — they register stubs on the same server: ```kotlin val mokksy = Mokksy().start() // load shared stubs from file mokksy.loadStubsFromFile("shared-stubs.yaml") // add test-specific stubs via DSL mokksy.get { path("/health") } respondsWithStatus HttpStatusCode.OK ``` ```java Mokksy mokksy = Mokksy.create().start(); // load shared stubs from file mokksy.loadStubsFromFile("shared-stubs.yaml"); // add test-specific stubs via Java API mokksy.get("/health").respondsWith(builder -> builder.body("OK")); ``` # Docker The `mokksy/server-jvm` image runs Mokksy as a standalone HTTP mock server. Stubs are loaded from a YAML file at startup, before the server begins accepting connections. ## Quick start Create a stubs file: ```yaml stubs: - name: ping method: GET path: /ping response: body: '{"response":"Pong"}' status: 200 ``` Start the container, mounting the file to the default config path: ```bash docker run -p 8080:8080 \ -v ./stubs.yaml:/config/stubs.yaml \ mokksy/server-jvm:snapshot ``` The image sets `MOKKSY_CONFIG=/config/stubs.yaml` by default, so mounting the file there requires no extra environment variables. ## Docker Compose ```yaml services: mokksy: image: mokksy/server-jvm ports: - "8080:8080" volumes: - ./stubs.yaml:/config/stubs.yaml ``` To use a different path, override `MOKKSY_CONFIG`: ```yaml services: mokksy: image: mokksy/server-jvm ports: - "8080:8080" volumes: - ./stubs.yaml:/app/stubs.yaml environment: MOKKSY_CONFIG: /app/stubs.yaml ``` ## Environment variables | Variable | Default | Description | |----------|---------|-------------| | `MOKKSY_CONFIG` | `/config/stubs.yaml` | Path to the YAML stubs file inside the container | | `JAVA_OPTS` | `-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError` | JVM flags passed to the server process | ## Startup behaviour Stubs are loaded from `MOKKSY_CONFIG` **before** the server binds its port. Every request is matchable from the first connection — there is no window where a request can arrive before stubs are registered. If `MOKKSY_CONFIG` is unset or the file is absent, the server starts with an empty stub registry and returns `404` for all requests. ## Stubs file format See [File-based configuration](../file-config/) for the full YAML schema, supported response types, and matching options. # Ktor integration If you already own a [Ktor][ktor] `Application` — a test harness with authentication middleware, custom plugins, or routes that must coexist with stubs — use the `mokksy` extension functions to mount stub handling directly, without allocating a second embedded server. ## Application-level installation `Application.mokksy(server)` installs [SSE][sse], `DoubleReceive`, and `ContentNegotiation` automatically, then mounts a catch-all route that dispatches every incoming request through the stub registry: ```kotlin import dev.mokksy.mokksy.MokksyServer import dev.mokksy.mokksy.mokksy import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty val server = MokksyServer() server.get { path("/ping") } respondsWith { body = "pong" } embeddedServer(Netty, port = 8080) { mokksy(server) }.start(wait = true) ``` Use this overload when Mokksy owns the entire application and you want the simplest possible setup. ## Route-level installation `Route.mokksy(server)` mounts the stub handler inside an existing route scope. Unlike the application-level overload, it does **not** install plugins — you are responsible for installing `SSE`, `DoubleReceive`, and `ContentNegotiation` on the surrounding application. This makes it suitable when Mokksy stubs coexist with real routes: ```kotlin routing { get("/health") { call.respondText("OK") } mokksy(server) } ``` To place stubs behind an authentication check, install the required plugins and wrap `mokksy` in an `authenticate` block: ```kotlin install(SSE) install(DoubleReceive) install(ContentNegotiation) { json() } install(Authentication) { basic("auth-basic") { validate { credentials -> if (credentials.name == "user" && credentials.password == "pass") { UserIdPrincipal(credentials.name) } else null } } } routing { authenticate("auth-basic") { mokksy(server) } } ``` Both extension functions accept any `path` pattern as a second parameter (default: `"{...}"`, which matches all routes). Narrow the scope by passing a prefix: ```kotlin mokksy(server, path = "/api/{...}") ``` [sse]: https://html.spec.whatwg.org/multipage/server-sent-events.html "Server-Side Events Specification" [ktor]: https://ktor.io # Anthropic [![Maven Central](https://img.shields.io/maven-central/v/dev.mokksy.aimocks/ai-mocks-anthropic.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/dev.mokksy.aimocks/ai-mocks-anthropic) [MockAnthropic](https://github.com/mokksy/ai-mocks/blob/main/ai-mocks-anthropic/src/commonMain/kotlin/dev/mokksy/aimocks/anthropic/MockAnthropic.kt) provides a local mock server for simulating [Anthropic API endpoints](https://docs.anthropic.com/en/api). It simplifies testing by allowing you to define request expectations and responses without making real network calls. ## Quick Start ### Add Dependency Include the library in your test dependencies (Maven or Gradle). ```kotlin testImplementation("dev.mokksy.aimocks:ai-mocks-anthropic-jvm:$latestVersion") ``` ```xml dev.mokksy.aimocks ai-mocks-anthropic-jvm [LATEST_VERSION] test ``` ### Initialize the Server ```kotlin val anthropic = MockAnthropic(verbose = true) ``` - The server will start on a random free port by default. - You can retrieve the server's base URL via `anthropic.baseUrl()`. ### Configure Requests and Responses Here's an example that sets up a mock "messages" endpoint and defines the response: ```kotlin anthropic.messages { temperature = 0.42 model = "claude-3-7-sonnet-latest" maxTokens = 100 topP = 0.95 topK = 40 userId = "user123" systemMessageContains("helpful assistant") userMessageContains("say 'Hello!'") } responds { messageId = "msg_1234567890" assistantContent = "Hello" // response content delay = 200.milliseconds // simulate delay stopReason = "end_turn" // reason for stopping } ``` - The `messages { ... }` block sets how the incoming request must look. - The `responds { ... }` block defines what the mock server returns. ### Calling Anthropic API Client Here's an example that sets up and calls the official [Anthropic SDK client](https://github.com/anthropics/anthropic-sdk-java): ```kotlin // create Anthropic SDK client val client = AnthropicOkHttpClient .builder() .apiKey("my-anthropic-api-key") .baseUrl(anthropic.baseUrl()) .build() // prepare Anthropic SDK call val params = MessageCreateParams .builder() .temperature(0.42) .maxTokens(100) .system("You are a helpful assistant.") .addUserMessage("Just say 'Hello!' and nothing else") .model("claude-3-7-sonnet-latest") .build() val result = client .messages() .create(params) result .content() .first() .asText() .text() shouldBe "Hello" // kotest matcher ``` ## Streaming Responses You can also configure streaming responses (such as chunked SSE events) for testing: ```kotlin anthropic.messages { temperature = 0.7 model = "claude-3-7-sonnet-latest" maxTokens = 150 topP = 0.95 topK = 40 userId = "user123" systemMessageContains("person from 60s") userMessageContains("What do we need?") } respondsStream { responseChunks = listOf("All", " we", " need", " is", " Love") delay = 50.milliseconds delayBetweenChunks = 10.milliseconds stopReason = "end_turn" } ``` Or, you can use a flow to generate the response: ```kotlin anthropic.messages("anthropic-messages-flow") { temperature = 0.7 model = "claude-3-7-sonnet-latest" maxTokens = 150 topP = 0.95 topK = 40 userId = "user123" systemMessageContains("person from 60s") userMessageContains("What do we need?") } respondsStream { responseFlow = flow { emit("All") emit(" we") emit(" need") emit(" is") emit(" Love") } delay = 60.milliseconds delayBetweenChunks = 15.milliseconds stopReason = "end_turn" } ``` Call Anthropic client: ```kotlin val params = MessageCreateParams .builder() .temperature(0.7) .maxTokens(150) .topP(0.95) .topK(40) .metadata(Metadata.builder().userId("user123").build()) .system("You are a person from 60s") .addUserMessage("What do we need?") .model("claude-3-7-sonnet-latest") .build() val timedValue = measureTimedValue { client .messages() .createStreaming(params) .use { streamResponse -> streamResponse.stream().count() } } timedValue.duration shouldBeLessThan 10.seconds timedValue.value shouldBeLessThan 10L ``` Use your Anthropic client to invoke the endpoint at `anthropic.baseUrl()`, and it will receive a streamed response. ## Error Simulation To test client behavior for exceptional cases: ```kotlin anthropic.messages { // expected request } respondsError { httpStatus = HttpStatusCode.InternalServerError // Set an error status code body = """{ "type": "error", "error": { "type": "api_error", "message": "An unexpected error has occurred internal to Anthropic's systems." } }""" // Optionally add a delay or other properties } ``` ## Practical Example in Tests ```kotlin @Test fun `test basic conversation`() { // Arrange: mock the messages API anthropic.messages { userMessageContains("Hello") } responds { assistantContent = "Hi from mock!" } // Act: call the mocked endpoint in your test code val result = yourAnthropicClient.sendMessage("Hello") // Assert: verify the response assertEquals("Hi from mock!", result.assistantMessage) } ``` ## Integration with LangChain4j You may use also LangChain4J Kotlin Extensions: ```kotlin // Set up mock response anthropic.messages { userMessageContains("Hello") } responds { assistantContent = "Hello" delay = 42.milliseconds } // Create the LangChain4j model val model: AnthropicChatModel = AnthropicChatModel .builder() .apiKey("foo") .baseUrl(anthropic.baseUrl() + "/v1") .modelName("claude-3-5-haiku-20241022") .build() // Make the request using Kotlin DSL val result = model.chat { messages += userMessage("Say Hello") } // Verify the response result.apply { finishReason() shouldBe FinishReason.STOP tokenUsage() shouldNotBe null aiMessage().text() shouldBe "Hello" } ``` ### Stream Responses Mock streaming responses easily with flow support: ```kotlin // Example 1: Using responseChunks val userMessage = "What do we need?" anthropic.messages { systemMessageContains("You are a person of 60s") userMessageContains(userMessage) } respondsStream { responseChunks = listOf("All", " we", " need", " is", " Love") } // Example 2: Using responseFlow val userMessage2 = "What is in the sea?" anthropic.messages { systemMessageContains("You are a person of 60s") userMessageContains(userMessage2) } respondsStream { responseFlow = flow { emit("Yellow") emit(" submarine") } } // Create the streaming model val model: AnthropicStreamingChatModel = AnthropicStreamingChatModel .builder() .apiKey("foo") .baseUrl(anthropic.baseUrl() + "/v1") .modelName("claude-3-5-haiku-20241022") .build() // Method 1: Using Kotlin Flow API model .chatFlow { messages += systemMessage("You are a person of 60s") messages += userMessage(userMessage2) }.buffer(capacity = 8096) .collect { when (it) { is StreamingChatModelReply.PartialResponse -> { println("token = ${it.partialResponse}") } is StreamingChatModelReply.CompleteResponse -> { println("Completed: $it") } is StreamingChatModelReply.Error -> { println("Error: $it") } } } // Method 2: Using Java-style API with a handler model.chat( ChatRequest .builder() .messages( systemMessage("You are a person of 60s"), userMessage(userMessage2) ) .build(), object : StreamingChatResponseHandler { override fun onCompleteResponse(completeResponse: ChatResponse) { println("Received CompleteResponse: $completeResponse") } override fun onPartialResponse(partialResponse: String) { println("Received partial response: $partialResponse") } override fun onError(error: Throwable) { println("Received error: $error") } } ) ``` ## Stopping the Server ```kotlin anthropic.shutdown() ``` Stops the mock server and frees up resources. # OpenAI [![Maven Central](https://img.shields.io/maven-central/v/dev.mokksy.aimocks/ai-mocks-openai.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/dev.mokksy.aimocks/ai-mocks-openai) AI-Mocks OpenAI is a specialized mock server implementation for mocking the OpenAI API, built using Mokksy. `MockOpenai` is tested against official [openai-java SDK](https://github.com/openai/openai-java) and popular JVM AI frameworks: [LangChain4j](https://github.com/langchain4j/langchain4j) and [Spring AI](https://docs.spring.io/spring-ai/reference/api/chatclient.html). Currently, it supports: - [Chat Completions](https://platform.openai.com/docs/api-reference/chat/create) (streaming and non-streaming) - [Embeddings](https://platform.openai.com/docs/api-reference/embeddings/create) - [Moderations](https://platform.openai.com/docs/api-reference/moderations/create) ## Quick Start Include the library in your test dependencies (Maven or Gradle). ```kotlin testImplementation("dev.mokksy.aimocks:ai-mocks-openai-jvm:$latestVersion") ``` ```xml dev.mokksy.aimocks ai-mocks-openai-jvm [LATEST_VERSION] test ``` ## Chat Completions API Set up a mock server and define mock responses: ```kotlin val openai = MockOpenai(verbose = true) ``` Let's simulate OpenAI [Chat Completions API](https://platform.openai.com/docs/api-reference/chat): ```kotlin // Define mock response openai.completion { temperature = 0.7 seed = 42 model = "gpt-4o-mini" maxTokens = 100 topP = 0.95 systemMessageContains("helpful assistant") userMessageContains("say 'Hello!'") } responds { assistantContent = "Hello" finishReason = "stop" delay = 200.milliseconds // delay before answer } // OpenAI client setup val client: OpenAIClient = OpenAIOkHttpClient .builder() .apiKey("dummy-api-key") .baseUrl(openai.baseUrl()) // connect to mock OpenAI .responseValidation(true) .build() // Use the mock endpoint val params = ChatCompletionCreateParams .builder() .temperature(0.7) .maxCompletionTokens(100) .topP(0.95) .messages( listOf( ChatCompletionMessageParam.ofSystem( ChatCompletionSystemMessageParam .builder() .content( "You are a helpful assistant.", ).build(), ), ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam .builder() .content("Just say 'Hello!' and nothing else") .build(), ), ), ).model("gpt-4o-mini") .build() val result: ChatCompletion = client .chat() .completions() .create(params) println(result) ``` ## Mocking Negative Scenarios With AI-Mocks it is possible to test negative scenarios, such as erroneous responses and delays. ### Custom Error Response ```kotlin openai.completion { temperature = 0.7 seed = 42 model = "gpt-4o-mini" maxTokens = 100 systemMessageContains("helpful assistant") userMessageContains("say 'Hello!'") }.respondsError(String::class) { body = // language=json """ { "type": "error", "code": "ERR_SOMETHING", "message": "Arrr, blast me barnacles! This be not what ye expect! 🏴‍☠️", "param": null } """.trimIndent() contentType = ContentType.Text.Plain delay = 100.milliseconds httpStatus = HttpStatusCode.PreconditionFailed } ``` ### OpenAI-Compatible Error Response ```kotlin openai.completion { temperature = 0.7 seed = 42 model = "gpt-4o-mini" maxTokens = 100 systemMessageContains("helpful assistant") userMessageContains("say 'Hello!'") }.respondsError(String::class) { body = // language=json """ { "error": { "type": "server_error", "code": "ERR_SOMETHING", "message": "Arrr, blast me barnacles! This be not what ye expect! 🏴‍☠️", "param": "foo" } } """.trimIndent() delay = 150.milliseconds contentType = ContentType.Application.Json httpStatus = HttpStatusCode.InternalServerError } ``` ## Integration with LangChain4j You may use also LangChain4J Kotlin Extensions: ```kotlin val model: OpenAiChatModel = OpenAiChatModel .builder() .apiKey("dummy-api-key") .baseUrl(openai.baseUrl()) .build() val result = model.chat { parameters = OpenAiChatRequestParameters .builder() .temperature(0.7) .modelName("gpt-4o-mini") .maxCompletionTokens(100) .topP(0.95) .seed(42) .build() messages += userMessage("Say Hello") } println(result) ``` ### Stream Responses Mock streaming responses easily with flow support or a list of chunks. #### Streaming with List of Chunks ```kotlin openai.completion { temperature = 0.7 model = "gpt-4o-mini" topP = 0.95 } respondsStream { responseChunks = listOf("All", " we", " need", " is", " Love") delay = 50.milliseconds delayBetweenChunks = 10.milliseconds finishReason = "stop" } // Create OpenAI client val client: OpenAIClient = OpenAIOkHttpClient .builder() .apiKey("dummy-key") .baseUrl(openai.baseUrl()) .build() // Make streaming request val params = ChatCompletionCreateParams .builder() .temperature(0.7) .topP(0.95) .messages( listOf( ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam .builder() .content("What do we need?") .build(), ), ), ).model("gpt-4o-mini") .build() val result = StringBuilder() client .chat() .completions() .createStreaming(params) .use { response -> response .stream() .flatMap { it.choices().stream() } .flatMap { it.delta().content().stream() } .forEach { result.append(it) } } // Result: "All we need is Love" ``` #### Streaming with Kotlin Flow ```kotlin openai.completion { temperature = 0.7 model = "gpt-4o-mini" } respondsStream { responseFlow = flow { emit("All") emit(" we") emit(" need") emit(" is") emit(" Love") } delay = 60.milliseconds delayBetweenChunks = 15.milliseconds finishReason = "stop" } ``` ## Integration with Spring-AI To test Spring-AI integration: ```kotlin // create mock server val openai = MockOpenai(verbose = true) // create Spring-AI client val chatClient = ChatClient .builder( org.springframework.ai.openai.OpenAiChatModel .builder() .openAiApi( OpenAiApi .builder() .apiKey("demo-key") .baseUrl(openai.baseUrl()) .build(), ).build(), ).build() // Set up a mock for the LLM call openai.completion { temperature = 0.7 seed = 42 model = "gpt-4o-mini" maxTokens = 100 topP = 0.95 topK = 40 systemMessageContains("helpful pirate") userMessageContains("say 'Hello!'") } responds { assistantContent = "Ahoy there, matey! Hello!" finishReason = "stop" delay = 200.milliseconds } // Configure Spring-AI client call val response = chatClient .prompt() .system("You are a helpful pirate") .user("Just say 'Hello!'") .options( OpenAiChatOptions .builder() .maxCompletionTokens(100) .temperature(0.7) .topP(0.95) .model("gpt-4o-mini") .seed(42) .build(), ) // Make a call .call() .chatResponse() // Verify the response response?.result shouldNotBe null response?.result?.apply { metadata.finishReason shouldBe "STOP" output.text shouldBe "Ahoy there, matey! Hello!" } ``` Check for examples in the [integration tests](https://github.com/mokksy/ai-mocks/tree/main/ai-mocks-openai/src/jvmTest/kotlin/dev/mokksy/aimocks/openai/springai). ## Embeddings API Mock the OpenAI [Embeddings API](https://platform.openai.com/docs/api-reference/embeddings) to test your embeddings generation: ### Basic Embedding Response ```kotlin // Set up mock server val openai = MockOpenai(verbose = true) // Define mock response for embedding request openai.embeddings { model = "text-embedding-3-small" inputContains("Hello") stringInput("Hello world") } responds { delay = 200.milliseconds embeddings( listOf(0.1f, 0.2f, 0.3f) ) } // Create OpenAI client val client: OpenAIClient = OpenAIOkHttpClient .builder() .apiKey("dummy-key") .baseUrl(openai.baseUrl()) .responseValidation(true) .build() // Make embedding request val params = EmbeddingCreateParams .builder() .model("text-embedding-3-small") .input(EmbeddingCreateParams.Input.ofString("Hello world")) .build() val result = client .embeddings() .create(params) // Verify results result.model() // "text-embedding-3-small" result.data()[0].embedding() // [0.1, 0.2, 0.3] result.data()[0].index() // 0 ``` ### Multiple Embeddings You can mock multiple embeddings for batch input: ```kotlin openai.embeddings { model = "text-embedding-3-small" stringListInput(listOf("Hello", "world")) } responds { delay = 100.milliseconds embeddings( listOf(0.1f, 0.2f, 0.3f), listOf(0.4f, 0.5f, 0.6f) ) } val params = EmbeddingCreateParams .builder() .model("text-embedding-3-small") .input(EmbeddingCreateParams.Input.ofArrayOfStrings(listOf("Hello", "world"))) .build() val result = client .embeddings() .create(params) // Returns 2 embeddings result.data().size // 2 result.data()[0].embedding() // [0.1, 0.2, 0.3] result.data()[1].embedding() // [0.4, 0.5, 0.6] ``` ### Advanced Input Matching You can use `inputContains()` to match requests where the input contains specific substrings: ```kotlin openai.embeddings { model = "text-embedding-3-small" inputContains("Hello") inputContains("world") stringInput("Hello world") } responds { embeddings(listOf(0.1f, 0.2f, 0.3f)) } ``` ### Error Scenarios Test error handling for embeddings: ```kotlin openai.embeddings { model = "text-embedding-3-small" stringInput("boom") }.respondsError(String::class) { body = "Kaboom!" contentType = ContentType.Text.Plain httpStatus = HttpStatusCode.BadRequest delay = 200.milliseconds } // This will throw BadRequestException val params = EmbeddingCreateParams .builder() .model("text-embedding-3-small") .input(EmbeddingCreateParams.Input.ofString("invalid input")) .build() try { client.embeddings().create(params) } catch (e: BadRequestException) { // Handle error } ``` ## Moderations API Mock the OpenAI [Moderations API](https://platform.openai.com/docs/api-reference/moderations) to test content moderation: ### Basic Moderation Response ```kotlin // Set up mock server val openai = MockOpenai(verbose = true) // Define mock response for moderation request openai.moderation { model = "omni-moderation-latest" inputContains("Hello world") } responds { flagged = true delay = 200.milliseconds category(name = "harassment", score = 0.1, inputTypes = listOf(TEXT)) category( name = ModerationCategory.SEXUAL, score = 0.2, inputTypes = listOf(TEXT, InputType.IMAGE) ) } // Create OpenAI client val client: OpenAIClient = OpenAIOkHttpClient .builder() .apiKey("dummy-key") .baseUrl(openai.baseUrl()) .responseValidation(true) .build() // Make moderation request val params = ModerationCreateParams .builder() .model("omni-moderation-latest") .input("Hello world") .build() val result = client .moderations() .create(params) // Verify results result.model() // "omni-moderation-latest" result.results()[0].flagged() // true result.results()[0].categories().harassment() // true result.results()[0].categoryScores().harassment() // 0.1 result.results()[0].categoryAppliedInputTypes().harassment() // [TEXT] ``` ### Moderation Error Scenarios ```kotlin openai.moderation { model = "omni-moderation-latest" inputContains("boom") }.respondsError(String::class) { body = "Kaboom!" contentType = ContentType.Text.Plain httpStatus = HttpStatusCode.BadRequest delay = 200.milliseconds } // This will throw BadRequestException val params = ModerationCreateParams .builder() .model("omni-moderation-latest") .input("boom") .build() try { client.moderations().create(params) } catch (e: BadRequestException) { // Handle error } ``` # Gemini [![Maven Central](https://img.shields.io/maven-central/v/dev.mokksy.aimocks/ai-mocks-gemini.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/dev.mokksy.aimocks/ai-mocks-gemini) AI-Mocks Gemini is a specialized mock server implementation for mocking the Google Vertex AI Gemini API, built using Mokksy. `MockGemini` is tested against the Spring AI framework with the Vertex AI Gemini integration. Currently, it supports basic content generation requests and streaming responses. ## Quick Start Include the library in your test dependencies (Maven or Gradle). ```kotlin testImplementation("dev.mokksy.aimocks:ai-mocks-gemini-jvm:$latestVersion") ``` ```xml dev.mokksy.aimocks ai-mocks-gemini-jvm [LATEST_VERSION] test ``` ## Content Generation API Set up a mock server and define mock responses: ```kotlin val gemini = MockGemini(verbose = true) ``` Let's simulate Gemini content generation API: ```kotlin // Define mock response gemini.generateContent { temperature = 0.7 model = "gemini-2.0-flash" project = "your-project-id" location = "us-central1" apiVersion = "v1beta1" path = null // custom request path, overrides "apiVersion" seed = 42 maxTokens = 100 topK = 40 topP = 0.95 maxOutputTokens(200) systemMessageContains("helpful pirate") userMessageContains("say 'Hello!'") requestBodyContains("helpful") requestBodyContainsIgnoringCase("PIRATE") requestBodyDoesNotContains("unwanted text") requestBodyDoesNotContainsIgnoringCase("unwanted case insensitive text") requestMatchesPredicate { it.generationConfig?.topP == 0.95 } } responds { content = "Ahoy there, matey! Hello!" finishReason = "stop" role = "model" delay = 42.milliseconds // delay before answer } ``` ### Configuration Options The following tables list all available configuration options for mocking Gemini API calls. #### Request Configuration Options | Option | Description | |------------------------------------------|------------------------------------------------------------------------------------| | `temperature` | Controls randomness of the output. Lower values make output more deterministic. | | `model` | The Gemini model to use. | | `maxTokens` | Maximum number of tokens to generate. | | `topK` | Limits token selection to the K most likely next tokens. | | `topP` | Limits token selection to tokens with cumulative probability of P. | | `project` | Google Cloud project ID. | | `location` | Google Cloud location. | | `apiVersion` | API version to use. | | `path` | Custom request path. | | `seed` | Seed for deterministic generation. | | `maxOutputTokens` | Maximum number of tokens to generate. | | `systemMessageContains` | Matches requests with system messages containing the specified text. | | `userMessageContains` | Matches requests with user messages containing the specified text. | | `requestBodyContains` | Matches requests with bodies containing the specified text. | | `requestBodyContainsIgnoringCase` | Matches requests with bodies containing the specified text (case-insensitive). | | `requestBodyDoesNotContains` | Matches requests with bodies not containing the specified text. | | `requestBodyDoesNotContainsIgnoringCase` | Matches requests with bodies not containing the specified text (case-insensitive). | | `requestMatchesPredicate` | Matches requests satisfying a custom predicate. | #### Response Configuration Options | Option | Description | Default Value | |----------------|--------------------------------------------------------|----------------------------------------------| | `content` | The content to include in the response. | `"This is a mock response from Gemini API."` | | `finishReason` | The reason why the model stopped generating tokens. | `"STOP"` | | `role` | The role of the content. | `"model"` | | `delay` | The delay before sending the response. | `Duration.ZERO` | | `delayMillis` | The delay before sending the response in milliseconds. | N/A | #### Streaming Content Generation Here's an example of setting up a streaming content generation mock: ```kotlin // Define streaming mock response gemini.generateContentStream { temperature = 0.7 model = "gemini-2.0-flash" project = "your-project-id" location = "us-central1" apiVersion = "v1beta1" seed = 42 maxTokens = 100 topK = 40 topP = 0.95 maxOutputTokens(200) systemMessageContains("helpful pirate") userMessageContains("say 'Hello!'") } respondsStream { responseFlow = flow { emit("Ahoy") emit(" there,") delay(100.milliseconds) emit(" matey!") emit(" Hello!") } // Alternatively, you can use responseChunks = listOf("Ahoy", " there,", " matey!", " Hello!") // Or chunks("Ahoy", " there,", " matey!", " Hello!") finishReason = "stop" delay = 60.milliseconds // delay before first chunk delayBetweenChunks = 15.milliseconds // delay between chunks } ``` #### Streaming Response Configuration Options | Option | Description | Default Value | |----------------------|----------------------------------------------------------------|-----------------| | `responseFlow` | A flow of content chunks to include in the streaming response. | `null` | | `responseChunks` | A list of content chunks to include in the streaming response. | `null` | | `chunks` | Sets the chunks of content for the streaming response. | N/A | | `delayBetweenChunks` | The delay between sending chunks. | `Duration.ZERO` | | `finishReason` | The reason why the model stopped generating tokens. | `"STOP"` | ## Integration with Spring-AI First, we need a function to create VertexAI client, configured to use the arbitrary server endpoint and credentials. ```kotlin internal fun createTestVertexAI( endpoint: String, projectId: String, location: String, timeout: Duration, ): VertexAI { try { val channelProvider = LlmUtilityServiceStubSettings .defaultHttpJsonTransportProviderBuilder() .setEndpoint(endpoint) .build() val newHttpJsonBuilder = LlmUtilityServiceStubSettings.newHttpJsonBuilder() newHttpJsonBuilder.unaryMethodSettingsBuilders().forEach { builder -> builder.setSimpleTimeoutNoRetriesDuration(timeout.toJavaDuration()) } val llmUtilityServiceStubSettings = newHttpJsonBuilder .setEndpoint(endpoint) .setCredentialsProvider(NoCredentialsProvider.create()) .setTransportChannelProvider(channelProvider) .build() val llmUtilityServiceClient = LlmUtilityServiceClient.create( LlmUtilityServiceSettings.create(llmUtilityServiceStubSettings), ) val predictionServiceSettingsBuilder = PredictionServiceSettings .newHttpJsonBuilder() .setEndpoint(endpoint) .setCredentialsProvider(NoCredentialsProvider.create()) .applyToAllUnaryMethods { updater -> updater.setSimpleTimeoutNoRetriesDuration(timeout.toJavaDuration()) as? Void? } val predictionServiceSettings = predictionServiceSettingsBuilder.build() val predictionClient = PredictionServiceClient.create(predictionServiceSettings) return VertexAI .Builder() .setTransport(Transport.REST) .setProjectId(projectId) .setLocation(location) .setLlmClientSupplier { llmUtilityServiceClient } .setPredictionClientSupplier { predictionClient } .setCredentials(ApiKeyCredentials.create("dummy-key")) .build() } catch (e: IOException) { throw RuntimeException(e) } } ``` Then we should create `MockGemini` server and test Spring-AI integration: ```kotlin // create mock server val gemini = MockGemini(verbose = true) // Create a VertexAI client that connects to the mock server val vertexAI = createTestVertexAI( endpoint = gemini.baseUrl(), projectId = "your-project-id", location = "us-central1", timeout = 5.seconds, ) // create Spring-AI client val chatClient = ChatClient .builder( VertexAiGeminiChatModel .builder() .vertexAI(vertexAI) .build(), ).build() // Set up a mock for the LLM call gemini.generateContent { temperature = 0.7 model = "gemini-2.0-flash" project = "your-project-id" location = "us-central1" systemMessageContains("You are a helpful pirate") userMessageContains("Just say 'Hello!'") } responds { content = "Ahoy there, matey! Hello!" finishReason = "stop" delay = 42.milliseconds } // Configure Spring-AI client call val response = chatClient .prompt() .system("You are a helpful pirate") .user("Just say 'Hello!'") .options(VertexAiGeminiChatOptions.builder().temperature(0.7).build()) // Make a call .call() .chatResponse() // Verify the response response shouldNotBeNull { result shouldNotBeNull { metadata.finishReason shouldBe "STOP" output.text shouldBe "Ahoy there, matey! Hello!" } } ``` ## Streaming Responses Mock streaming responses easily with flow support: ```kotlin // configure mock gemini gemini.generateContentStream { temperature = 0.7 model = "gemini-2.0-flash" project = "your-project-id" location = "us-central1" systemMessageContains("You are a helpful pirate") userMessageContains("Just say 'Hello!'") }.respondsStream(sse = false) { responseFlow = flow { emit("Ahoy") emit(" there,") delay(100.milliseconds) emit(" matey!") emit(" Hello!") } delay = 60.milliseconds delayBetweenChunks = 50.milliseconds } // Use Spring AI's streaming API val buffer = StringBuffer() val chunkCount = chatClient .prompt() .system("You are a helpful pirate") .user("Just say 'Hello!'") .options(VertexAiGeminiChatOptions.builder().temperature(0.7).build()) .stream() .chatResponse() .doOnNext { chunk -> // Process each chunk as it arrives chunk.result.output.text?.let(buffer::append) }.count() .block(5.seconds.toJavaDuration()) // Verify the complete response buffer.toString() shouldBe "Ahoy there, matey! Hello!" ``` ## Integration with Google Gen AI Java SDK AI-Mocks Gemini can also be used to test applications that use the [Google Gen AI Java SDK](https://github.com/googleapis/java-genai) directly. ### Setting up the Client First, create a mock Gemini server: ```kotlin val gemini = MockGemini(verbose = true) ``` Then, configure the Google Gen AI Java SDK client to use the mock server: ```kotlin val client = Client.builder() .project("your-project-id") .location("us-central1") .credentials( GoogleCredentials.create( AccessToken.newBuilder().setTokenValue("dummy-token").build() ) ) .vertexAI(true) .httpOptions(HttpOptions.builder().baseUrl(gemini.baseUrl()).build()) .build() ``` ### Regular Content Generation Set up a mock response for a regular content generation request: ```kotlin gemini.generateContent { temperature = 0.7 seed = 42 model = "gemini-2.0-flash" project = "your-project-id" location = "us-central1" apiVersion = "v1beta1" systemMessageContains("You are a helpful pirate") userMessageContains("Just say 'Hello!'") } responds { content = "Ahoy there, matey! Hello!" delay = 60.milliseconds } ``` Make a request using the Google Gen AI Java SDK: ```kotlin val config = GenerateContentConfig.builder() .seed(42) .maxOutputTokens(100) .temperature(0.7f) .systemInstruction( Content.builder().role("system") .parts(Part.fromText("You are a helpful pirate")).build() ) .build() val response = client.models.generateContent( "gemini-2.0-flash", "Just say 'Hello!'", config ) // Verify the response response.text() shouldBe "Ahoy there, matey! Hello!" ``` ### Streaming Content Generation Set up a mock response for a streaming content generation request: ```kotlin gemini.generateContentStream { temperature = 0.7 apiVersion = "v1beta1" location = "us-central1" maxOutputTokens(100) model = "gemini-2.0-flash" project = "your-project-id" seed = 42 systemMessageContains("You are a helpful pirate") userMessageContains("Just say 'Hello!'") } respondsStream { responseFlow = flow { emit("Ahoy") emit(" there,") delay(100.milliseconds) emit(" matey!") emit(" Hello!") } delay = 60.milliseconds delayBetweenChunks = 15.milliseconds } ``` Make a streaming request using the Google Gen AI Java SDK: ```kotlin val response = client.models.generateContentStream( "gemini-2.0-flash", "Just say 'Hello!'", config ) // Collect and verify the streaming response val fullResponse = response.joinToString(separator = "") { it.text() ?: "" } fullResponse shouldBe "Ahoy there, matey! Hello!" ``` Check for examples in the [integration tests](https://github.com/mokksy/ai-mocks/tree/main/ai-mocks-gemini/src/jvmTest/kotlin/dev/mokksy/aimocks/gemini). # Ollama [![Maven Central](https://img.shields.io/maven-central/v/dev.mokksy.aimocks/ai-mocks-ollama.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/dev.mokksy.aimocks/ai-mocks-ollama) AI-Mocks Ollama is a specialized mock server implementation for mocking the [Ollama API](https://github.com/ollama/ollama/blob/main/docs/api.md), built using Mokksy. `MockOllama` is tested against the [LangChain4j](https://github.com/langchain4j/langchain4j) framework with the Ollama integration. Currently, it supports the main endpoints of the Ollama API, including: - Generate completions - Chat completions - Model management - Embeddings ## Quick Start Include the library in your test dependencies (Maven or Gradle). ```kotlin testImplementation("dev.mokksy.aimocks:ai-mocks-ollama-jvm:$latestVersion") ``` ```xml dev.mokksy.aimocks ai-mocks-ollama-jvm [LATEST_VERSION] test ``` ## Basic Setup Set up a mock server and define mock responses: ```kotlin // Create a mock Ollama server val ollama = MockOllama(verbose = true) // Get the base URL of the mock server val baseUrl = ollama.baseUrl() ``` ## Generate Completions API Let's simulate Ollama's Generate Completions API: ```kotlin // Define mock response ollama.generate { model = "llama3" userMessageContains("Tell me a joke") } responds { content("Why did the chicken cross the road? To get to the other side!") doneReason("stop") delay = 42.milliseconds } // Create request val request = GenerateRequest( model = "llama3", prompt = "Tell me a joke", stream = false, options = ModelOptions(temperature = 0.7, topP = 0.9) ) // Send request to mock server val httpRequest = HttpRequest.newBuilder() .uri(URI.create("${ollama.baseUrl()}/api/generate")) .header("Content-Type", "application/json") .POST( HttpRequest.BodyPublishers.ofString( json.encodeToString(GenerateRequest.serializer(), request) ) ) .build() val response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()) // Verify response response.statusCode() shouldBe 200 val generateResponse = json.decodeFromString(response.body()) generateResponse.response shouldBe "Why did the chicken cross the road? To get to the other side!" generateResponse.model shouldBe "llama3" generateResponse.done shouldBe true generateResponse.doneReason shouldBe "stop" ``` ## Chat Completions API Let's simulate Ollama's Chat Completions API: ```kotlin // Define mock response ollama.chat { model = "llama3" userMessageContains("Hello") } responds { content("Hello, how can I help you today?") delay = 42.milliseconds } // Create request val request = ChatRequest( model = "llama3", messages = listOf( Message( role = "user", content = "Hello" ) ), stream = false, options = ModelOptions(temperature = 0.7, topP = 0.9) ) // Send request to mock server val httpRequest = HttpRequest.newBuilder() .uri(URI.create("${ollama.baseUrl()}/api/chat")) .header("Content-Type", "application/json") .POST( HttpRequest.BodyPublishers.ofString( json.encodeToString(ChatRequest.serializer(), request) ) ) .build() val response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()) // Verify response response.statusCode() shouldBe 200 val chatResponse = json.decodeFromString(response.body()) chatResponse.message.content shouldBe "Hello, how can I help you today?" chatResponse.model shouldBe "llama3" chatResponse.done shouldBe true ``` ## Embeddings API Let's simulate Ollama's Embeddings API: ```kotlin // Define mock response for a single string input val embeddings = listOf(listOf(0.1f, 0.2f, 0.3f, 0.4f, 0.5f)) ollama.embed { model = "llama3" stringInput = "The sky is blue" } responds { embeddings(embeddings) delay = 42.milliseconds } // Create request val request = EmbeddingsRequest( model = "llama3", input = listOf("The sky is blue"), options = ModelOptions(temperature = 0.7, topP = 0.9) ) // Send request to mock server val httpRequest = HttpRequest.newBuilder() .uri(URI.create("${ollama.baseUrl()}/api/embed")) .header("Content-Type", "application/json") .POST( HttpRequest.BodyPublishers.ofString( json.encodeToString(EmbeddingsRequest.serializer(), request) ) ) .build() val response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()) // Verify response response.statusCode() shouldBe 200 val embedResponse = json.decodeFromString(response.body()) embedResponse.embeddings shouldBe embeddings embedResponse.model shouldBe "llama3" ``` You can also mock embeddings for a list of strings: ```kotlin // Define mock response for multiple string inputs val embeddings = listOf( listOf(0.1f, 0.2f, 0.3f, 0.4f, 0.5f), listOf(0.6f, 0.7f, 0.8f, 0.9f, 1.0f) ) ollama.embed { model = "llama3" stringListInput = listOf("The sky is blue", "The grass is green") } responds { embeddings(embeddings) delay = 42.milliseconds } // Create request val request = EmbeddingsRequest( model = "llama3", input = listOf("The sky is blue", "The grass is green"), options = ModelOptions(temperature = 0.7, topP = 0.9) ) // Send request to mock server val httpRequest = HttpRequest.newBuilder() .uri(URI.create("${ollama.baseUrl()}/api/embed")) .header("Content-Type", "application/json") .POST( HttpRequest.BodyPublishers.ofString( json.encodeToString(EmbeddingsRequest.serializer(), request) ) ) .build() val response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString()) // Verify response response.statusCode() shouldBe 200 val embedResponse = json.decodeFromString(response.body()) embedResponse.embeddings shouldBe embeddings embedResponse.model shouldBe "llama3" ``` ## Streaming Responses AI-Mocks-Ollama supports streaming responses for both generate and chat endpoints: ```kotlin // Define streaming mock response for generate endpoint ollama.generate { model = "llama3" stream = true userMessageContains("Tell me a story") } respondsStream { responseChunks = listOf( "Once upon a time", " in a land far, far away", " there lived a programmer", " who never had to debug in production." ) delayBetweenChunks = 100.milliseconds } // Define streaming mock response for chat endpoint ollama.chat { model = "llama3" stream = true } respondsStream { responseChunks = listOf( "Hello", ", how can I", " help you today?" ) delayBetweenChunks = 100.milliseconds } ``` ## Request Configuration Options The following tables list the available configuration options for mocking Ollama API calls. ### Generate Request Configuration Options | Option | Description | |---------------------|--------------------------------------------| | `model` | The model to match in the request | | `prompt` | The prompt to match in the request | | `system` | The system message to match in the request | | `template` | The template to match in the request | | `stream` | Whether to match streaming requests | | `requestBodyString` | Adds a string matcher for the request body | ### Chat Request Configuration Options | Option | Description | |---------------------|-----------------------------------------------| | `model` | The model to match in the request | | `messages` | The messages to match in the request | | `stream` | Whether to match streaming requests | | `requestBodyString` | Adds a string matcher for the request body | | `userMessage` | Adds a user message to match in the request | | `systemMessage` | Adds a system message to match in the request | ### Embed Request Configuration Options | Option | Description | |---------------------|------------------------------------------------------------| | `model` | The model to match in the request | | `stringInput` | The string input to match in the request | | `stringListInput` | The list of string inputs to match in the request | | `truncate` | Whether to truncate the input to fit within context length | | `options` | Additional model parameters to match in the request | | `keepAlive` | Controls how long the model will stay loaded into memory | | `requestBodyString` | Adds a string matcher for the request body | ## Response Configuration Options ### Generate Response Configuration Options | Option | Description | Default Value | |--------------|--------------------------------------------------------------|------------------------------------------| | `content` | The content to include in the response | `"This is a mock response from Ollama."` | | `doneReason` | The reason why generation completed (e.g., "stop", "length") | `"stop"` | | `delay` | The delay before sending the response | `Duration.ZERO` | ### Chat Response Configuration Options | Option | Description | Default Value | |-------------|-------------------------------------------|------------------------------------------| | `content` | The content to include in the response | `"This is a mock response from Ollama."` | | `thinking` | The thinking process of the model | `null` | | `toolCalls` | The tool calls to include in the response | `null` | | `delay` | The delay before sending the response | `Duration.ZERO` | ### Embed Response Configuration Options | Option | Description | Default Value | |--------------|-----------------------------------------------|------------------------------------------------| | `embeddings` | The embeddings to include in the response | `listOf(listOf(0.1f, 0.2f, 0.3f, 0.4f, 0.5f))` | | `embedding` | A single embedding to include in the response | N/A | | `model` | The model name to include in the response | `null` | | `delay` | The delay before sending the response | `Duration.ZERO` | ### Streaming Response Configuration Options | Option | Description | Default Value | Availability | |----------------------|-----------------------------------------------------|-----------------|-----------------| | `responseFlow` | A flow of content chunks for the streaming response | `null` | Generate & Chat | | `responseChunks` | A list of content chunks for the streaming response | `null` | Generate & Chat | | `delayBetweenChunks` | The delay between sending chunks | `Duration.ZERO` | Generate & Chat | | `doneReason` | The reason why generation completed | `"stop"` | Generate only | ## Integration Testing Create a test class with a `MockOllama` instance to test your Ollama client integration: ```kotlin class MyOllamaTest { private val ollama = MockOllama() @Test fun `Should respond to Chat Completion`() = runTest { // Configure mock response ollama.chat { model = "llama3" } responds { content("Hello, how can I help you today?") } // Use your Ollama client to make a request and verify the response } } ``` ## Integration with LangChain4j AI-Mocks-Ollama can be used with LangChain4j's Ollama integration: ```kotlin // Create a mock Ollama server val ollama = MockOllama(verbose = true) // Configure mock response ollama.chat { model = "llama3" } responds { content("Hello, how can I help you today?") delay = 42.milliseconds } // Create LangChain4j Ollama client val model = OllamaChatModel.builder() .baseUrl(ollama.baseUrl()) .modelName("llama3") .temperature(0.7) .topP(0.9) .build() // Use LangChain4j Kotlin DSL to send a request val result = model.chat { messages += userMessage("Hello") } // Verify response result.apply { aiMessage().text() shouldBe "Hello, how can I help you today?" } ``` Check for examples in the [integration tests](https://github.com/mokksy/ai-mocks/tree/main/ai-mocks-ollama/src/jvmTest/kotlin/dev/mokksy/aimocks/ollama). # A2A Protocol [![Maven Central](https://img.shields.io/maven-central/v/dev.mokksy.aimocks/ai-mocks-a2a.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/dev.mokksy.aimocks/ai-mocks-a2a) [MockAgentServer](https://github.com/mokksy/ai-mocks/blob/main/ai-mocks-a2a/src/commonMain/kotlin/dev/mokksy/aimocks/a2a/MockAgentServer.kt) provides a local mock server for simulating [A2A (Agent-to-Agent) API](https://a2a-protocol.org/latest/specification/) endpoints. It simplifies testing by allowing you to define request expectations and responses without making real network calls. **NB!** The server only supports [JSON-RPC 2.0 transport](https://a2a-protocol.org/latest/specification/#321-json-rpc-20-transport). Supported A2A protocol version is **0.3.0**. ## Quick Start ### Add Dependency Include the library in your test dependencies (Maven or Gradle). ```xml dev.mokksy.aimocks ai-mocks-a2a-jvm [LATEST_VERSION] test ``` ```kotlin dependencies { testImplementation("me.kpavlov.aimocks:ai-mocks-a2a:0.x.x") // Optional: typed model classes testImplementation("me.kpavlov.aimocks:ai-mocks-a2a-models:0.x.x") } ``` ```groovy dependencies { testImplementation 'me.kpavlov.aimocks:ai-mocks-a2a:0.x.x' testImplementation 'me.kpavlov.aimocks:ai-mocks-a2a-models:0.x.x' } ``` ### Initialize the Server ```kotlin val a2aServer = MockAgentServer(verbose = true) ``` - The server will start on a random free port by default. - You can retrieve the server's base URL via `a2aServer.baseUrl()`. ## HTTP Client Setup You may use any HTTP client that supports Server-Sent Events (SSE) to make requests to the mock server. The AI-Mocks A2A library provides a convenient function to create a Ktor client configured for A2A: ```kotlin // Create a Ktor client configured for A2A val a2aClient = A2AClientFactory.create(baseUrl = a2aServer.baseUrl()) ``` Alternatively, you can create the client manually: ```kotlin // Create a Ktor client configured for A2A val a2aClient = HttpClient(Java) { val json = Json { prettyPrint = true isLenient = true } install(ContentNegotiation) { json(json) } install(SSE) { showRetryEvents() showCommentEvents() } install(DefaultRequest) { url(a2aServer.baseUrl()) // Set the base URL } } ``` ## Agent Card Endpoint The [Agent Card endpoint](https://a2a-protocol.org/latest/specification/#55-agentcard-object-structure) provides information about the agent's capabilities, skills, and authentication mechanisms. Remote Agents that support A2A are required to publish an **Agent Card** in JSON format describing the agent's capabilities/skills and authentication mechanism. Clients use the Agent Card information to identify the best agent that can perform a task and leverage A2A to communicate with that remote agent. Mock Server configuration: ```kotlin // Create an AgentCard object val agentCard = AgentCard.create { name = "test-agent" description = "test-agent-description" url = a2aServer.baseUrl() documentationUrl = "https://example.com/documentation" version = "0.0.1" provider { organization = "Acme, Inc." url = "https://example.com/organization" } capabilities { streaming = true pushNotifications = true stateTransitionHistory = true } skills += skill { id = "walk" name = "Walk the walk" description = "I can walk" tags = listOf("move") } skills += skill { id = "talk" name = "Talk the talk" description = "I can talk" tags = listOf("communicate") } } // Configure the mock server to respond with the AgentCard a2aServer.agentCard() responds { delay = 1.milliseconds card = agentCard } ``` Client call example: ```kotlin // Make a GET request to the Agent Card endpoint val response = a2aClient .get("/.well-known/agent-card.json") { }.call .response .body() // Parse the response into an AgentCard object val receivedCard = Json.decodeFromString(response) ``` ## Get Task Endpoint The [Get Task endpoint](https://a2a-protocol.org/latest/specification/#73-tasksget) allows clients to retrieve information about a specific task. Clients may use this method to retrieve the generated Artifacts for a Task. The agent determines the retention window for Tasks previously submitted to it. The client may also request the last N items of history of the Task which will include all Messages, in order, sent by client and server. Mock Server configuration: ```kotlin // Configure the mock server to respond with a task a2aServer.getTask() responds { id = 1 result { id = "tid_12345" contextId = "ctx_12345" status { state = "completed" } artifacts += artifact { name = "joke" parts += textPart { text = "This is a joke" } } } } ``` You can also configure the mock server to respond with an error: ```kotlin // Configure the mock server to respond with a task not found error a2aServer.getTask() responds { id = 1 error = taskNotFoundError() } ``` Client call example: ```kotlin // Create a GetTaskRequest object val jsonRpcRequest = GetTaskRequest( id = "1", params = TaskQueryParams( id = UUID.randomUUID().toString(), historyLength = 2, ), ) // Make a POST request to the Get Task endpoint val response = a2aClient .post("/") { contentType(ContentType.Application.Json) setBody(Json.encodeToString(jsonRpcRequest)) }.call .response // Parse the response into a GetTaskResponse object val body = response.body() val payload = Json.decodeFromString(body) ``` ## Send Message Endpoint The [Send Message endpoint](https://a2a-protocol.org/latest/specification/#71-messagesend) allows clients to send a message to the agent for processing. This method allows a client to send content to a remote agent to start a new Task, resume an interrupted Task or reopen a completed Task. A Task interrupt may be caused due to an agent requiring additional user input or a runtime error. Mock Server configuration: ```kotlin // Create a Task object val task = Task.create { id = "tid_12345" contextId = "ctx_12345" status { state = "completed" } artifact { name = "joke" parts += text { "This is a joke" } parts += file { uri = "https://example.com/readme.md" } parts += file { bytes = "1234".toByteArray() } parts += data { mapOf("foo" to "bar") } } } // Configure the mock server to respond with the task a2aServer.sendMessage() responds { id = 1 result = task } ``` Client call example: ```kotlin // Create a SendMessageRequest object using the builder function val jsonRpcRequest = sendMessageRequest { id = "1" params { message { role = Message.Role.user parts += text { "Tell me a joke" } parts += file { uri = "https://example.com/readme.md" } parts += file { bytes = "1234".toByteArray() } parts += data { mapOf("foo" to "bar") } } } } // Make a POST request to the Send Message endpoint val response = a2aClient .post("/") { contentType(ContentType.Application.Json) setBody(Json.encodeToString(jsonRpcRequest)) }.call .response // Parse the response into a SendMessageResponse object val body = response.body() val payload = Json.decodeFromString(body) ``` ## Send Message Streaming Endpoint The [Send Message Streaming endpoint](https://a2a-protocol.org/latest/specification/#72-messagestream) allows clients to send a message to the agent for processing and receive streaming updates. For clients and remote agents capable of communicating over HTTP with Server-Sent Events (SSE), clients can send the RPC request with method `message/stream` when creating a new Task. The remote agent can respond with a stream of TaskStatusUpdateEvents (to communicate status changes or instructions/requests) and TaskArtifactUpdateEvents (to stream generated results). Mock Server configuration: ```kotlin // Configure the mock server to respond with streaming updates val taskId = "task_12345" a2aServer.sendMessageStreaming() responds { delayBetweenChunks = 1.seconds responseFlow = flow { emit( taskStatusUpdateEvent { id = taskId status { state = "working" timestamp = Clock.System.now() } } ) emit( taskArtifactUpdateEvent { id = taskId artifact { name = "joke" parts += textPart { text = "This" } } } ) emit( taskArtifactUpdateEvent { id = taskId artifact { name = "joke" parts += textPart { text = "is" } append = true } } ) emit( taskArtifactUpdateEvent { id = taskId artifact { name = "joke" parts += textPart { text = "a" } append = true } } ) emit( taskArtifactUpdateEvent { id = taskId artifact { name = "joke" parts += textPart { text = "joke!" } append = true lastChunk = true } } ) emit( taskStatusUpdateEvent { id = taskId status { state = "completed" timestamp = Clock.System.now() } final = true } ) } } ``` Client call example: ```kotlin // Create a collection to store the events var collectedEvents = ConcurrentLinkedQueue() // Helper function to handle events fun handleEvent(event: TaskUpdateEvent): Boolean { when (event) { is TaskStatusUpdateEvent -> { println("Task status: $event") if (event.final) { return false } } is TaskArtifactUpdateEvent -> { println("Task artifact: $event") } } return true } // Make a POST request to the Send Message Streaming endpoint with SSE a2aClient.sse( request = { url { a2aServer.baseUrl() } method = HttpMethod.Post val payload = SendStreamingMessageRequest( id = "1", params = MessageSendParams.create { message { role = Message.Role.user parts += textPart { text = "Tell me a joke" } } }, ) body = TextContent( text = Json.encodeToString(payload), contentType = ContentType.Application.Json, ) }, ) { var reading = true while (reading) { incoming.collect { println("Event from server:\n$it") it.data?.let { val event = Json.decodeFromString(it) collectedEvents.add(event) if (!handleEvent(event)) { reading = false cancel("Finished") } } } } } ``` ## Cancel Task Endpoint The [Cancel Task endpoint](https://a2a-protocol.org/latest/specification/#74-taskscancel) allows clients to cancel a task that is in progress. A client may choose to cancel previously submitted Tasks, for example when the user no longer needs the result or wants to stop a long-running task. Mock Server configuration: ```kotlin // Configure the mock server to respond with a canceled task a2aServer.cancelTask() responds { id = 1 result { id = "tid_12345" contextId = UUID.randomUUID().toString() status = TaskStatus(state = "canceled") } } ``` Client call example: ```kotlin // Create a CancelTaskRequest object val jsonRpcRequest = cancelTaskRequest { id = "1" params { id = UUID.randomUUID().toString() } } // Make a POST request to the Cancel Task endpoint val response = a2aClient .post("/") { contentType(ContentType.Application.Json) setBody(Json.encodeToString(jsonRpcRequest)) }.call .response // Parse the response into a CancelTaskResponse object val body = response.body() val payload = Json.decodeFromString(body) ``` ## Set Task Push Notification Config Endpoint The [Set Task Push Notification endpoint](https://a2a-protocol.org/latest/specification/#75-taskspushnotificationconfigset) allows clients to configure push notifications for a task. Clients may configure a push notification URL for receiving updates on Task status changes. This is particularly useful for long-running tasks where the client may not want to maintain an open connection. Mock Server configuration: ```kotlin // Create a TaskPushNotificationConfig object val taskId: TaskId = "task_12345" val config = TaskPushNotificationConfig.create { id = taskId pushNotificationConfig { url = "https://example.com/callback" token = "abc.def.jk" authentication { credentials = "secret" schemes += "Bearer" } } } // Configure the mock server to respond with the config a2aServer.setTaskPushNotification() responds { id = 1 result { id = taskId pushNotificationConfig { url = "https://example.com/callback" token = "abc.def.jk" authentication { credentials = "secret" schemes += "Bearer" } } } } ``` Client call example: ```kotlin // Create a TaskPushNotificationConfig object val config = TaskPushNotificationConfig.create { id = "task_12345" pushNotificationConfig { url = "https://example.com/callback" token = "abc.def.jk" authentication { credentials = "secret" schemes += "Bearer" } } } // Create a SetTaskPushNotificationRequest object val jsonRpcRequest = SetTaskPushNotificationRequest( id = "1", params = config, ) // Make a POST request to the Set Task Push Notification endpoint val response = a2aClient .post("/") { contentType(ContentType.Application.Json) setBody(Json.encodeToString(jsonRpcRequest)) }.call .response // Parse the response into a SetTaskPushNotificationResponse object val body = response.body() val payload = Json.decodeFromString(body) ``` ## Get Task Push Notification Config Endpoint The [Get Task Push Notification endpoint](https://a2a-protocol.org/latest/specification/#76-taskspushnotificationconfigget) allows clients to retrieve the push notification configuration for a specific task. Clients may retrieve the currently configured push notification configuration for a Task using this method, which is useful for verifying or displaying the current notification settings. Mock Server configuration: ```kotlin // Create a TaskPushNotificationConfig object val taskId: TaskId = "task_12345" val config = TaskPushNotificationConfig( id = taskId, pushNotificationConfig = PushNotificationConfig( url = "https://example.com/callback", token = "abc.def.jk", authentication = AuthenticationInfo( schemes = listOf("Bearer"), ), ), ) // Configure the mock server to respond with the config a2aServer.getTaskPushNotification() responds { id = 1 result = config } ``` Client call example: ```kotlin // Create a GetTaskPushNotificationRequest object val jsonRpcRequest = GetTaskPushNotificationRequest( id = "1", params = TaskIdParams( id = taskId, ), ) // Make a POST request to the Get Task Push Notification endpoint val response = a2aClient .post("/") { contentType(ContentType.Application.Json) setBody(Json.encodeToString(jsonRpcRequest)) }.call .response // Parse the response into a GetTaskPushNotificationResponse object val body = response.body() val payload = Json.decodeFromString(body) ``` ## List Task Push Notification Config Endpoint The [List Task Push Notification Config endpoint](https://a2a-protocol.org/latest/specification/#77-taskspushnotificationconfiglist) allows clients to list configured push notification destinations. This can be useful to inspect or manage existing configurations. Mock Server configuration: ```kotlin // Configure the mock server to respond with a list of push notification configs val taskId: TaskId = "task_12345" a2aServer.listTaskPushNotificationConfig() responds { id = 1 result = listOf( TaskPushNotificationConfig.create { id = taskId pushNotificationConfig { url = "https://example.com/callback" token = "abc.def.jk" authentication { schemes += "Bearer" } } } ) } ``` Client call example: ```kotlin // Build a ListTaskPushNotificationConfigRequest val jsonRpcRequest = ListTaskPushNotificationConfigRequest( id = "1", params = ListTaskPushNotificationConfigParams.create { limit(10) offset(0) }, ) // Make a POST request to the List Task Push Notification Config endpoint val response = a2aClient .post("/") { contentType(ContentType.Application.Json) setBody(Json.encodeToString(jsonRpcRequest)) }.call .response // Parse the response val body = response.body() val payload = Json.decodeFromString(body) ``` ## Delete Task Push Notification Config Endpoint The [Delete Task Push Notification Config endpoint](https://a2a-protocol.org/latest/specification/#78-taskspushnotificationconfigdelete) allows clients to delete the configured push notification destination for a task. Mock Server configuration: ```kotlin // Configure the mock server to respond to delete push notification config val taskId: TaskId = "task_12345" a2aServer.deleteTaskPushNotificationConfig() responds { id = 1 // success without error } ``` Client call example: ```kotlin // Build a DeleteTaskPushNotificationConfigRequest val jsonRpcRequest = DeleteTaskPushNotificationConfigRequest( id = "1", params = deleteTaskPushNotificationConfigParams { id(taskId) }, ) // Make a POST request to the Delete Task Push Notification Config endpoint val response = a2aClient .post("/") { contentType(ContentType.Application.Json) setBody(Json.encodeToString(jsonRpcRequest)) }.call .response // Parse the response val body = response.body() val payload = Json.decodeFromString(body) ``` ## Task Resubscription Endpoint The [Task Resubscription endpoint](https://a2a-protocol.org/latest/specification/#79-tasksresubscribe) allows clients to resubscribe to streaming updates for a task that was previously created. This is useful when a client loses connection and needs to resume receiving updates for an ongoing task. A disconnected client may resubscribe to a remote agent that supports streaming to receive Task updates via Server-Sent Events (SSE). Mock Server configuration: ```kotlin // Configure the mock server to respond with streaming updates val taskId: TaskId = "task_12345" a2aServer.taskResubscription() responds { delayBetweenChunks = 1.seconds responseFlow = flow { emit( taskStatusUpdateEvent { id = taskId status { state = "working" timestamp = Clock.System.now() } } ) emit( taskArtifactUpdateEvent { id = taskId artifact { name = "joke" parts += textPart { text = "This is a resubscribed joke!" } lastChunk = true } } ) emit( taskStatusUpdateEvent { id = taskId status { state = "completed" timestamp = Clock.System.now() } final = true } ) } } ``` Client call example: ```kotlin // Create a collection to store the events val collectedEvents = ConcurrentLinkedQueue() // Helper function to handle events fun handleEvent(event: TaskUpdateEvent): Boolean { when (event) { is TaskStatusUpdateEvent -> { println("Task status: $event") if (event.final) { return false } } is TaskArtifactUpdateEvent -> { println("Task artifact: $event") } } return true } // Make a POST request to the Task Resubscription endpoint with SSE a2aClient.sse( request = { url { a2aServer.baseUrl() } method = HttpMethod.Post contentType(ContentType.Application.Json) val payload = TaskResubscriptionRequest( id = "1", params = TaskQueryParams( id = taskId, ), ) setBody(payload) }, ) { var reading = true while (reading) { incoming.collect { println("Event from server:\n$it") it.data?.let { val event = Json.decodeFromString(it) collectedEvents.add(event) if (!handleEvent(event)) { reading = false cancel("Finished") } } } } } ``` ## Testing Push Notifications The A2A protocol supports push notifications, which allow agents to notify clients of updates outside a connected session. This is particularly useful for long-running tasks where the client may not want to maintain an open connection. ### Accessing Task Notification History You can access the notification history for a specific task using the `getTaskNotifications` method: ```kotlin val taskId: TaskId = "task_12345" val notificationHistory = a2aServer.getTaskNotifications(taskId) // Verify that the history is initially empty notificationHistory.events() shouldHaveSize 0 ``` ### Sending Push Notifications You can send push notifications using the `sendPushNotification` method: ```kotlin val taskUpdateEvent = taskArtifactUpdateEvent { id = taskId artifact { name = "joke" parts += textPart { text = "This is a notification joke!" } lastChunk = true } } a2aServer.sendPushNotification(event = taskUpdateEvent) ``` ### Verifying Notifications You can verify that notifications were received by checking the notification history: ```kotlin // Verify that the notification history contains the event notificationHistory.events() shouldContain taskUpdateEvent ``` ## Verifying Requests After your test is complete, you can verify that all expected requests were received: ```kotlin a2aServer.verifyNoUnexpectedRequests() ``` This ensures that your test made all the expected requests to the mock server. # Spring Boot Use Mokksy in Spring Boot when your application calls an external HTTP dependency through configuration, `RestClient`, `WebClient`, or another HTTP client bean. ```text Spring Boot test -> application HTTP client -> Mokksy -> stubbed external API ``` ## Typical setup 1. Start Mokksy in the test fixture. 2. Inject `mokksy.baseUrl()` into the property your application uses for the external base URL. 3. Register stubs before the application code makes the call. 4. Execute the real Spring Boot behavior and verify the request journal. In practice, step 2 usually means overriding the Spring configuration property that holds the outbound service URL during test startup, rather than changing production bean wiring by hand. This works well for payment gateways, fraud or risk APIs, telecom provisioning services, document pipelines, and internal platform dependencies. ## Where to go next This is a routing guide for Spring Boot projects. Use the linked Mokksy pages for the tested stub and verification APIs, and override your application's external base-URL property in the test profile or test fixture. - [First integration test](../../mokksy/first-integration-test/) for the end-to-end test shape - [Stubbing responses](../../mokksy/stubbing/) for response DSL examples - [Request matching](../../mokksy/request-matching/) for path, header, and body matching If the dependency is OpenAI, Anthropic, Gemini, Ollama, or A2A, use [AI-Mocks](../../ai-mocks/) instead of raw Mokksy. # Quarkus Quarkus follows the same base-URL replacement pattern as Spring Boot: start Mokksy before the test, configure the application to call `mokksy.baseUrl()`, then exercise the real Quarkus HTTP client code. In practice, that usually means overriding the Quarkus config value that normally holds the external service base URL during test startup, rather than swapping clients manually inside the application. ```text Quarkus test -> application HTTP client -> Mokksy -> stubbed external API ``` Use this for standard HTTP integrations as well as AI provider clients that sit behind Quarkus services. ## Demos - ["LangChain4j with Quarkus (KotlinConf`25)"](https://2025.kotlinconf.com/talks/795976/) - ["Financial Assistant Chatbot with Easy RAG"](https://github.com/kpavlov/quarkus-assistant-demo) - ["Quarkus LC4J Demo"](https://github.com/kpavlov/quarkus-ai-demo/tree/main#demo-2---service-with-mock-openai) ## Where to go next This is a routing guide for Quarkus projects. Use the linked Mokksy and AI-Mocks pages for the tested APIs, and override the outbound base-URL configuration value in your Quarkus test setup. - [Mokksy overview](../../mokksy/) for core HTTP and SSE mocks - [LangChain4j](../langchain4j/) if the Quarkus service uses LangChain4j - [OpenAI SDK](../openai-sdk/) or [Anthropic SDK](../anthropic-sdk/) for provider SDK clients # LangChain4j Use AI-Mocks when your LangChain4j code talks to a provider API. Mokksy supplies the underlying HTTP and SSE behavior, and AI-Mocks adds provider-compatible request and response shapes. ## Supported provider guides This is a routing guide for LangChain4j. Choose the provider page that matches the model client configured in your application. - [OpenAI with LangChain4j](../../ai-mocks/openai/#integration-with-langchain4j) - [Anthropic with LangChain4j](../../ai-mocks/anthropic/#integration-with-langchain4j) - [Ollama with LangChain4j](../../ai-mocks/ollama/#integration-with-langchain4j) ## Demo - ["LangChain4j with Quarkus (KotlinConf`25)"](https://2025.kotlinconf.com/talks/795976/) - ["Financial Assistant Chatbot with Easy RAG"](https://github.com/kpavlov/quarkus-assistant-demo) ## Product choice - Use [Mokksy](../../mokksy/) directly for generic HTTP dependencies. - Use [AI-Mocks](../../ai-mocks/) for provider-backed LangChain4j tests. # Spring AI Spring AI sits on top of provider APIs, so the correct integration point is AI-Mocks rather than plain Mokksy. Use the provider-specific guide that matches your Spring AI client. ## Supported provider guides This is a routing guide for Spring AI. Choose the provider page that matches the Spring AI client configured in your application. - [OpenAI with Spring AI](../../ai-mocks/openai/#integration-with-spring-ai) - [Gemini with Spring AI](../../ai-mocks/gemini/#integration-with-spring-ai) These guides cover provider-compatible request formats, streaming behavior, and error handling without live provider credentials, rate limits, or provider outages. ## Product choice - Use [Mokksy](../../mokksy/) for general HTTP services in Spring applications. - Use [AI-Mocks](../../ai-mocks/) for Spring AI clients. # OpenAI Java SDK Use [AI-Mocks OpenAI](../../ai-mocks/openai/) when production code calls the official [`openai-java`](https://github.com/openai/openai-java) SDK. Your test runs the real SDK client against a local OpenAI-compatible endpoint by replacing only the base URL and using a dummy credential. ```text Integration test -> openai-java client -> AI-Mocks OpenAI -> Mokksy HTTP/SSE server ``` The examples below follow the official SDK integration tests in the AI-Mocks repository: [Kotlin chat and streaming tests](https://github.com/mokksy/ai-mocks/tree/main/ai-mocks-openai/src/jvmTest/kotlin/dev/mokksy/aimocks/openai/official) and the [Java chat test](https://github.com/mokksy/ai-mocks/blob/main/ai-mocks-openai/src/jvmTest/java/dev/mokksy/aimocks/openai/MockOpenaiJavaTest.java). ## Configure the client Point the official SDK client to `openai.baseUrl()`. The API key is required by the SDK builder, but no live OpenAI credential is used because requests go to the local mock server. ```kotlin import com.openai.client.OpenAIClient import com.openai.client.okhttp.OpenAIOkHttpClient import dev.mokksy.aimocks.openai.MockOpenai val openai = MockOpenai(verbose = true) val client: OpenAIClient = OpenAIOkHttpClient.builder() .apiKey("dummy-key-for-tests") .baseUrl(openai.baseUrl()) .responseValidation(true) .build() ``` ```java import com.openai.client.OpenAIClient; import com.openai.client.okhttp.OpenAIOkHttpClient; import dev.mokksy.aimocks.openai.MockOpenai; var openai = new MockOpenai(); OpenAIClient client = OpenAIOkHttpClient.builder() .apiKey("dummy-key-for-tests") .baseUrl(openai.baseUrl()) .build(); ``` ## Test a chat completion Register the expected provider request with AI-Mocks, then make the SDK call through the configured client. The test fails if application code sends a request that does not match the stub. ```kotlin import com.openai.models.chat.completions.ChatCompletionCreateParams import com.openai.models.chat.completions.ChatCompletionMessageParam import com.openai.models.chat.completions.ChatCompletionUserMessageParam import io.kotest.matchers.shouldBe openai.completion { model = "gpt-4o-mini" userMessageContains("say 'Hello!'") } responds { assistantContent = "Hello" finishReason = "stop" } val params = ChatCompletionCreateParams.builder() .messages( listOf( ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam.builder() .content("Just say 'Hello!' and nothing else") .build() ) ) ) .model("gpt-4o-mini") .build() val result = client.chat().completions().create(params) result.choices().first().message().content().orElseThrow() shouldBe "Hello" ``` ```java import com.openai.core.JsonValue; import com.openai.models.ChatModel; import com.openai.models.chat.completions.ChatCompletionCreateParams; import com.openai.models.chat.completions.ChatCompletionMessageParam; import com.openai.models.chat.completions.ChatCompletionUserMessageParam; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; openai.completion(req -> { req.model("gpt-4o-mini"); req.requestBodyContains("say 'Hey!'"); }).responds(response -> { response.assistantContent("Hey!"); response.finishReason("stop"); }); var params = ChatCompletionCreateParams.builder() .messages(List.of(ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam.builder() .role(JsonValue.from("user")) .content("Just say 'Hey!'").build()))) .model(ChatModel.GPT_4O_MINI) .build(); var result = client.chat().completions().create(params); assertThat(result.choices().get(0).message().content()).hasValue("Hey!"); ``` ## Test streaming behavior The repository also tests `client.chat().completions().createStreaming(...)` against `openai.completion { ... } respondsStream { ... }`, including delays before the first response and between chunks. Use that path when application behavior depends on incremental delivery rather than only the final message. See the runnable [OpenAI streaming examples](../../ai-mocks/openai/#stream-responses) for the complete Kotlin setup. ## Covered provider surfaces The AI-Mocks OpenAI integration tests exercise the official SDK with: - Chat Completions, including streaming completions - Responses inputs - Embeddings - Moderations - HTTP error responses ## Next steps - [OpenAI provider reference](../../ai-mocks/openai/) for the mock DSL and supported endpoint examples - [Spring AI](../spring-ai/) if the SDK is hidden behind Spring AI - [LangChain4j](../langchain4j/) if the SDK is hidden behind LangChain4j - [Spring Boot](../spring-boot/) or [Quarkus](../quarkus/) for application-level base URL configuration - [Integrations overview](../) for all client and framework guides # Anthropic Java SDK Use [AI-Mocks Anthropic](../../ai-mocks/anthropic/) when production code calls the official Anthropic Java SDK. Point the real SDK client at `anthropic.baseUrl()` so tests execute provider-shaped HTTP and streaming requests locally. ```text Integration test -> Anthropic Java SDK -> AI-Mocks Anthropic -> Mokksy HTTP/SSE server ``` The official SDK API shapes shown below are backed by [Anthropic SDK integration tests](https://github.com/mokksy/ai-mocks/tree/main/ai-mocks-anthropic/src/jvmTest/kotlin/dev/mokksy/aimocks/anthropic/official). This repository currently verifies the official Anthropic SDK from Kotlin; it also verifies LangChain4j usage separately. ## Configure the client The SDK requires an API key value during construction. Because `baseUrl()` routes the request to the local mock server, use a dummy value in tests rather than a live provider credential. ```kotlin import com.anthropic.client.AnthropicClient import com.anthropic.client.okhttp.AnthropicOkHttpClient import dev.mokksy.aimocks.anthropic.MockAnthropic val anthropic = MockAnthropic(verbose = true) val client: AnthropicClient = AnthropicOkHttpClient.builder() .apiKey("dummy-key-for-tests") .baseUrl(anthropic.baseUrl()) .responseValidation(true) .build() ``` ## Test the Messages API Register the request criteria and deterministic reply before invoking `client.messages().create(...)`. ```kotlin import com.anthropic.models.messages.MessageCreateParams import io.kotest.matchers.shouldBe import kotlin.jvm.optionals.getOrNull anthropic.messages { model = "claude-3-7-sonnet-latest" maxTokens = 100 systemMessageContains("helpful assistant") userMessageContains("say 'Hello!'") } responds { messageId = "msg_test" assistantContent = "Hello" } val params = MessageCreateParams.builder() .model("claude-3-7-sonnet-latest") .maxTokens(100) .system("You are a helpful assistant.") .addUserMessage("Just say 'Hello!' and nothing else") .build() val result = client.messages().create(params) result.content().mapNotNull { it.text().getOrNull() }.first().text() shouldBe "Hello" ``` ## Test streaming Messages The official SDK tests also configure streamed message content and consume it through `client.messages().createStreaming(...)`: ```kotlin import com.anthropic.models.messages.MessageCreateParams import io.kotest.matchers.collections.shouldContainExactly import kotlin.time.Duration.Companion.milliseconds val tokens = listOf("All", " we", " need", " is", " Love") anthropic.messages { model = "claude-3-7-sonnet-latest" userMessageContains("What do we need?") } respondsStream { responseChunks = tokens delay = 50.milliseconds delayBetweenChunks = 10.milliseconds stopReason = "end_turn" } val params = MessageCreateParams.builder() .model("claude-3-7-sonnet-latest") .maxTokens(100) .addUserMessage("What do we need?") .build() val received = mutableListOf() client.messages().createStreaming(params).use { response -> response.stream() .filter { it.isContentBlockDelta() } .forEachOrdered { chunk -> received += chunk.asContentBlockDelta().delta().asText().text() } } received shouldContainExactly tokens ``` ## Next steps - [Anthropic provider reference](../../ai-mocks/anthropic/) for the mock DSL, streaming options, and error responses - [LangChain4j](../langchain4j/) if your application uses Anthropic through LangChain4j - [Spring Boot](../spring-boot/) or [Quarkus](../quarkus/) for application-level base URL configuration - [Integrations overview](../) for all client and framework guides # Koog Koog is an AI framework, so the appropriate AI-Mocks module depends on the provider configured in your application. Use the corresponding [AI-Mocks provider guide](../../ai-mocks/) when Koog is configured for a supported provider. The verified end-to-end example on this page is the OpenAI-backed pattern from the [`koog-spring-boot-assistant`](https://github.com/kpavlov/koog-spring-boot-assistant/tree/main/integration-tests/src/test/kotlin/com/example/it) integration tests. In that setup, Koog talks to an OpenAI-compatible provider, so the integration point is [AI-Mocks OpenAI](../../ai-mocks/openai/) rather than plain Mokksy. The tests start `MockOpenai`, point Koog at `mockOpenai.baseUrl()`, and exercise the real Spring Boot application through HTTP and WebSocket clients. ## Workflow context from the sample repo The sample application is not a single prompt-in, prompt-out flow. Its README describes a Koog strategy with moderation, request mapping, streaming LLM output, and tool execution. That context matters because the integration tests stub several provider endpoints, not just one chat response. ```mermaid --- title: streaming-strategy --- stateDiagram state "moderate-input" as moderate_input state "mapStringToRequests" as mapStringToRequests state "applyRequestToSession" as applyRequestToSession state "nodeStreaming" as nodeStreaming state "executeMultipleTools" as executeMultipleTools state "mapToolCallsToRequests" as mapToolCallsToRequests [*] --> moderate_input : transformed moderate_input --> mapStringToRequests : transformed moderate_input --> [*] : transformed mapStringToRequests --> applyRequestToSession applyRequestToSession --> nodeStreaming nodeStreaming --> executeMultipleTools : onCondition nodeStreaming --> [*] : onCondition executeMultipleTools --> mapToolCallsToRequests mapToolCallsToRequests --> applyRequestToSession ``` The same repository also exposes this graph through `/api/koog/strategy/graph`, and the integration tests assert that the endpoint returns Mermaid output for the running strategy. ## Inject the mock server into Koog The sample app starts `MockOpenai` once in the test environment, prepares deterministic embeddings for RAG ingestion, and then injects the mock base URL into Koog before Spring Boot starts: ```kotlin object TestEnvironment { val mockOpenai = MockOpenai(verbose = true) init { System.setProperty("OPENAI_API_KEY", "dummyOpenAIKey") System.setProperty("spring.profiles.active", "test") listOf( "Care for Magical Trees", "Valley of Light", "Magical Bow", "Morning Pine Elixir", "Teleportation and Portals", ).forEach { mockOpenai.embeddings { inputContains(it) } responds { delay = 1.milliseconds } } } } object Server { init { System.setProperty("ai.koog.openai.base-url", TestEnvironment.mockOpenai.baseUrl()) SpringApplication.run( com.example.app.Application::class.java, "--server.port=0", "--spring.profiles.active=test", ) } } ``` This keeps the real Koog and Spring Boot wiring intact while replacing the provider dependency with a deterministic local server. The sample application also performs embedding requests during startup for RAG ingestion. Those embedding stubs must exist before Spring Boot starts, or the application will make unmatched calls while the test environment is still booting. ## Test the full Koog request path The positive-path test in the sample repo drives the real application client, not Koog internals. It stubs embeddings, moderation, and the chat completion stream, then verifies the final answer: ```kotlin mockOpenai.embeddings { stringInput(question) } responds { delay = 40.milliseconds } mockOpenai.moderation { inputContains(question) } responds { flagged = false } mockOpenai.completion { systemMessageContains("witty and wise Elven assistant guiding adventurers") userMessageContains(question) } respondsStream { responseFlow = flowOf(expectedAnswer) } val response = chatClient.sendMessage(question) ``` That test shape is useful when you want to prove prompt routing, moderation checks, RAG lookups, and provider calls still produce the expected application response. ## Stream token-by-token output The same repo includes a WebSocket integration test that verifies streaming delivery timing. The mock server emits one token chunk at a time with a fixed delay between chunks: ```kotlin val delayBetweenChunks = 500.milliseconds mockOpenai.completion { systemMessageContains("witty and wise Elven assistant guiding adventurers") userMessageContains(question) } respondsStream { responseFlow = expectedTokens .asFlow() .onEach { delay(delayBetweenChunks) } } ``` The test then measures the WebSocket output and checks that Koog forwards the token stream with the expected pacing. This is the right place to catch buffering mistakes and streaming regressions. ## Exercise moderation and failure paths The sample repo does not stop at happy-path chat. It also verifies: - moderation blocking with `mockOpenai.moderation { ... } responds { flagged = true }` - embedding failures with `respondsError { httpStatusCode = ... }` - moderation API failures with fallback behavior - LLM request failures for both SSE and non-streaming completion paths For example, the failure test uses provider-like HTTP status codes such as `400`, `401`, `403`, `404`, `418`, `500`, and `503`, then verifies that the application returns a stable fallback message instead of crashing. ## Verify Koog-specific endpoints too The repo also tests a Koog strategy-graph endpoint by fetching `/api/koog/strategy/graph` and asserting that the response contains Mermaid state-diagram output. That is a useful pattern when your application exposes Koog diagnostics or graph introspection routes in addition to chat endpoints. ## Source and next steps - [Koog Spring Boot Assistant integration tests](https://github.com/kpavlov/koog-spring-boot-assistant/tree/main/integration-tests/src/test/kotlin/com/example/it) - [AI-Mocks providers](../../ai-mocks/) - [AI-Mocks OpenAI](../../ai-mocks/openai/) - [Spring Boot](../spring-boot/) - [OpenAI SDK](../openai-sdk/) # Mokksy vs WireMock WireMock remains a strong general-purpose HTTP stubbing tool. Mokksy focuses on Kotlin and Java integration tests where streaming behavior, Server-Sent-Events (SSE), and deterministic failure simulation matter. ## Comparison | Capability | Mokksy | WireMock | |------------|--------|----------| | HTTP stubbing and request matching | Yes | Yes | | SSE-specific response API | `respondsWithSseStream` with event chunks | Use WireMock response configuration or evaluate extensions for your SSE scenario | | Application-defined stream chunks | `respondsWithStream` accepts chunks or a flow | [Chunked Dribble Delay](https://wiremock.org/docs/simulating-faults/#chunked-dribble-delay) divides a configured body into chunks | | Inter-chunk timing | Direct `delayBetweenChunks` control | Chunk count and total response duration determine pacing | | Long-lived streams for client timeout tests | A flow can remain open with `awaitCancellation()` | Evaluate against your timeout scenario and WireMock setup | | HTTP status and delayed-response scenarios | Yes | Yes | | Connection-level fault injection | Not positioned as a core Mokksy API | Documented faults include malformed chunks and connection reset | | Verification API | Request journal and stub verification | Yes | | Kotlin-first DSL and Java API | Yes | General Java DSL; Kotlin use is through its Java API or integrations | | Provider-shaped AI API mocks | Available through AI-Mocks | Not a WireMock core provider toolkit | | Embedding in an existing Ktor application | Yes | Not a Mokksy-equivalent Ktor embedding API | WireMock capability statements above are based on its [official fault-simulation documentation](https://wiremock.org/docs/simulating-faults/). If your decision depends on a WireMock extension or a newer product surface, validate that specific setup before migrating. ## When to choose Mokksy - You test clients that consume SSE or streaming APIs. - You need Kotlin or Java tests that define SSE events and stream chunks directly in test code. - You want concise Kotlin DSLs and Java-friendly APIs in JVM test suites. - You use AI provider SDKs and want AI-Mocks on top of a real HTTP/SSE mock server. ## When WireMock may be enough - Your team already has a mature WireMock setup and its delay or fault APIs cover your scenarios. - You need connection-reset or malformed-response faults that WireMock already documents directly.