Skip to main content

testMediator

There are no mocks and no stubs in mediatork-test. A handler is just a lambda that returns a value, so a test double is just the real thing with a shorter handler body.

testMediator { } builds a real Mediator using the exact same DSL you use in production — handle, handleStream, notification, validate, behaviors. The mediator it returns (RecordingMediator) additionally records every request and notification so you can assert on what was dispatched.

import com.fajrbahr.mediatork.test.testMediator

val mediator = testMediator {
handle<GetUserQuery, UserModel> { UserModel("User-${it.id}") }
}

val user = mediator.send(GetUserQuery("42")) // runs the real pipeline
assertEquals("User-42", user.name)
assertEquals(1, mediator.sentOf<GetUserQuery>().size)

Best for ViewModel tests and integration tests where you control every response.


Request handlers

Register a handler body that returns a value. Both the request and result type are given explicitly (Kotlin can't infer the result type from a lambda literal):

val mediator = testMediator {
handle<GetUserQuery, UserModel> { query -> UserModel("User-${query.id}") }
handle<CreateOrderCommand, OrderResult> { cmd -> OrderResult(cmd.itemId) }
}

Throw from the body to simulate a failure — no special "throws" API:

val mediator = testMediator {
handle<CreateOrderCommand, OrderResult> { error("out of stock") }
}
assertFailsWith<IllegalStateException> { mediator.send(CreateOrderCommand("A1")) }

send throws MissingHandlerException if no handler is registered for the request type — exactly like production.

With a validator

Register a real validate<T> and invalid requests throw ValidationException before the handler runs, exactly like the real pipeline:

val mediator = testMediator {
handle<CreateOrderCommand, OrderResult> { OrderResult(it.itemId) }
validate<CreateOrderCommand> { cmd ->
rules { check(cmd.itemId.isNotBlank()) { "itemId required" } }
}
}

You can reuse a validator you already ship (e.g. validate<CreateOrderCommand>(createOrderValidator)) so the test exercises the same rules as the app.


Notification handlers

val received = mutableListOf<OrderPlacedEvent>()
val mediator = testMediator {
notification<OrderPlacedEvent> { received += it }
}

mediator.publish(OrderPlacedEvent("ORD-1"))

assertEquals(1, received.size)
assertEquals(1, mediator.publishedOf<OrderPlacedEvent>().size)

Publishing a notification with no registered listener throws MissingNotificationHandlerException — the real pipeline behaviour. Register a listener (even an empty one) for events your code fires, or set onMissingNotification in the block.


Stream handlers

val mediator = testMediator {
handleStream<StreamItemsQuery, String> { query -> flow { emit("${query.prefix}-1") } }
}
val items = mediator.stream(StreamItemsQuery("x")).toList()

Pipeline behaviors

Add real behaviors with the production behaviors(...) DSL — including the built-in behaviors:

val mediator = testMediator {
handle<GetUserQuery, UserModel> { UserModel(it.id) }
behaviors(logging(), timing())
}

RecordingMediator

testMediator { } returns a RecordingMediator: a thin wrapper that delegates every call to a real mediator and records what passed through. Use it directly to record around a mediator you assembled from production modules:

import com.fajrbahr.mediatork.test.RecordingMediator

val mediator = RecordingMediator(
mediatorK {
courseModule(store, deptStore, studentStore)
studentModule(studentStore, courseStore)
}
)

val service = CheckoutService(mediator)
service.placeOrder(cartId = "CART-1")

assertEquals("CART-1", mediator.sentOf<CreateOrderCommand>().first().cartId)

Recording API

MemberDescription
sentEvery request passed to send/stream, in order
publishedEvery notification passed to publish, in order
sentOf<T>()Recorded requests of type T, in order
publishedOf<T>()Recorded notifications of type T, in order

Assertions are just standard list checks (sentOf<T>().size, sent.isEmpty(), …) — no bespoke assertSent helpers to learn.


Choosing the right double

SituationUse
Test controls what send/publish dotestMediator { }
Test also asserts which requests were dispatchedtestMediator { } then sentOf<T>()
Test records around already-assembled modulesRecordingMediator(mediatorK { … })
Constructor needs a Mediator but send is never calledtestMediator { } (an empty real mediator)

There's no lenient no-op double on purpose: an empty testMediator { } is a real mediator, so an unexpected send fails loudly with MissingHandlerException instead of silently returning nothing.

Need something more specific? Mediator is a plain interface — implement, wrap, or decorate it directly.


Next

Testing ViewModels