mokksy

Mokksy - Mock HTTP Server, built with Kotlin and Ktor.
Check out the AI-Mocks project for advanced LLM and A2A protocol mocking capabilities.
[!NOTE] Mokksy server was a part of the AI-Mocks project and has now moved to a separate repository. No artefact relocation is required.
Table of Contents
- Why Mokksy?
- Key Features
- Quick start
- Responding with predefined responses
- Server-Side Events (SSE) response
- Request Specification Matchers
- Verifying Requests
- Request Journal
Why Mokksy?
Wiremock does not support true SSE and streaming responses.
Mokksy is here to address those limitations. Particularly, it might be useful for integration testing LLM clients.
Key Features
- Streaming Support: True support for streaming responses and Server-Side Events (SSE)
- Response Control: Flexibility to control server responses directly via
ApplicationCallobject - Delay Simulation: Support for simulating response delays and delays between chunks
- Modern API: Fluent Kotlin DSL API with Kotest Assertions
- Error Simulation: Ability to mock negative scenarios and error responses
Quick start
Add dependencies:
Gradle build.gradle.kts:
dependencies { // for multiplatform projects implementation("dev.mokksy:mokksy:$latestVersion") // for JVM projects implementation("dev.mokksy:mokksy-jvm:$latestVersion") }pom.xml:
<dependency> <groupId>dev.mokksy</groupId> <artifactId>mokksy-jvm</artifactId> <version>[LATEST_VERSION]</version> <scope>test</scope> </dependency>
Create and start Mokksy server:
JVM (blocking):
val mokksy = Mokksy().apply { runBlocking { startSuspend() } }Configure http client using Mokksy server's as baseUrl in your application:
val client = HttpClient {
install(DefaultRequest) {
url(mokksy.baseUrl())
}
}
Responding with predefined responses
Mokksy supports all HTTP verbs. Here are some examples.
GET request
GET request example:
// 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
When the request does not match - Mokksy server returns 404 (Not Found):
val notFoundResult = client.get("/ping") {
headers.append("Foo", "baz")
}
notFoundResult.status shouldBe HttpStatusCode.NotFound
POST request
POST request example:
// 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.status shouldBe HttpStatusCode.Created
result.bodyAsText() shouldBe expectedResponse
result.headers["Location"] shouldBe "/things/$id"
result.headers["Foo"] shouldBe "bar"
Server-Side Events (SSE) response
Server-Side Events (SSE) is a technology that allows a server to push updates to the client over a single, long-lived HTTP connection. This enables real-time updates without requiring the client to continuously poll the server for new data.
SSE streams events in a standardized format, making it easy for clients to consume the data and handle events as they arrive. It's lightweight and efficient, particularly well-suited for applications requiring real-time updates like live notifications or feed updates.
Server-Side Events (SSE) example:
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.status shouldBe HttpStatusCode.OK
result.contentType() shouldBe ContentType.Text.EventStream.withCharsetIfNeeded(Charsets.UTF_8)
result.bodyAsText() shouldBe "data: One\r\ndata: Two\r\n"
Request Specification Matchers
Mokksy provides various matcher types to specify conditions for matching incoming HTTP requests:
- Path matchers —
path("/things")orpath = beEqual("/things") - Header matchers —
containsHeader("X-Request-ID", "abc")checks for a header with an exact value - Content matchers —
bodyContains("value")checks if the raw body string contains a substring;bodyString += contain("value")adds a Kotest matcher directly - Predicate matchers —
bodyMatchesPredicate { it?.name == "foo" }matches against the typed, deserialized request body - Call matchers —
successCallMatchermatches if a function called with the body does not throw - Priority —
priority = 10onRequestSpecificationBuildersets theRequestSpecification.priorityof the stub; lower values indicate higher priority. Default isInt.MAX_VALUE. When multiple stubs match a request, the one with the highest priority (lowest numerical value) is selected.
Priority Example
If multiple stubs match, the one with the lower priority value wins:
// Catch-all stub with low priority (high value)
mokksy.get {
path = contain("/things")
priority = 99
} respondsWith {
body = "Generic Thing"
}
// Specific stub with high priority (low 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"
Verifying Requests
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.
// Fails if any stub has matchCount == 0
mokksy.verifyNoUnmatchedStubs()
Note: Be careful when running tests in parallel against a single
MokksyServerinstance. Some stubs might be unmatched when one test completes. Avoid calling this in@AfterEach/@AfterTestunless 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.
// Fails if any request arrived with no matching stub
mokksy.verifyNoUnexpectedRequests()
Recommended AfterEach setup
Run both checks after every test to catch a mismatch in either direction:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MyTest {
val mokksy = Mokksy()
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 = beEqual("/hi")
} respondsWith {
body = "Hello"
delay(100.milliseconds)
}
// 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.shutdownSuspend()
}
}
Inspecting unmatched items
Use the find* variants to retrieve the unmatched items directly for custom assertions:
// List<RecordedRequest> — HTTP requests with no matching stub
val unmatchedRequests: List<RecordedRequest> = mokksy.findAllUnexpectedRequests()
// List<RequestSpecification<*>> — stubs that were never triggered
val unmatchedStubs: List<RequestSpecification<*>> = mokksy.findAllUnmatchedStubs()
RecordedRequest is an immutable snapshot that captures method, uri, and headers of the incoming request.
Request Journal
Mokksy records incoming requests in a RequestJournal. The recording mode is controlled by JournalMode in
ServerConfiguration:
| Mode | Behaviour |
|---|---|
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. |
val mokksy = Mokksy(
configuration = ServerConfiguration(
journalMode = JournalMode.FULL,
),
)
Call resetMatchCounts() between scenarios to clear both stub match counts and the journal:
@AfterTest
fun afterEach() {
mokksy.resetMatchCounts()
}

