Skip to main content

MediatorSpy

MediatorSpy wraps any Mediator and records every send and publish call while still delegating to the real handlers underneath. No mocking library needed.

Use it when you want to assert what was dispatched — which requests were sent, which notifications were published, and how many times — while your actual handler logic still runs and returns real results.


Setup

// Wrap any real mediator — FakeMediator, or your production one
val spy = MediatorSpy(
FakeMediator {
+CreateOrderHandler()
+FetchUserHandler()
}
)

Inject spy wherever your production code expects a Mediator. All send and publish calls go through it.


Asserting requests were sent

@Test
fun `checkout sends CreateOrderCommand`() = runTest {
val spy = MediatorSpy(FakeMediator { +CreateOrderHandler() })
val checkout = CheckoutService(spy)

checkout.placeOrder(cartId = "CART-1")

// at least one was sent
spy.assertSent<CreateOrderCommand>()

// exact count
spy.assertSentCount<CreateOrderCommand>(1)

// inspect the actual value
val cmd = spy.sentOf<CreateOrderCommand>().first()
assertEquals("CART-1", cmd.cartId)
}

Asserting nothing was sent

@Test
fun `checkout does not send command when cart is empty`() = runTest {
val spy = MediatorSpy(FakeMediator { +CreateOrderHandler() })
val checkout = CheckoutService(spy)

checkout.placeOrder(cartId = "") // empty cart → no command

spy.assertNotSent<CreateOrderCommand>()
}

Asserting notifications were published

@Test
fun `order placement publishes OrderPlacedEvent`() = runTest {
val fake = FakeMediator {
+CreateOrderHandler()
registerNotification(object : NotificationHandler<OrderPlacedEvent> {
override suspend fun handle(notification: OrderPlacedEvent) = Unit
})
}
val spy = MediatorSpy(fake)
val checkout = CheckoutService(spy)

checkout.placeOrder(cartId = "CART-1")

spy.assertPublished<OrderPlacedEvent>()
assertEquals("CART-1", spy.publishedOf<OrderPlacedEvent>().first().cartId)
}

Full spy API

MemberDescription
sentRequestsAll requests passed to send, in order
publishedNotificationsAll notifications passed to publish, in order
sentOf<T>()Filtered list of sent requests of type T
publishedOf<T>()Filtered list of published notifications of type T
assertSent<T>(message?)Fails if no request of type T was sent
assertNotSent<T>(message?)Fails if any request of type T was sent
assertSentCount<T>(n, message?)Fails if sent count ≠ n
assertPublished<T>(message?)Fails if no notification of type T was published
assertNotPublished<T>(message?)Fails if any notification of type T was published
assertPublishedCount<T>(n, message?)Fails if published count ≠ n
reset()Clears all recorded sends and publishes

Resetting between scenarios

@Test
fun `two independent scenarios in one test`() = runTest {
val spy = MediatorSpy(FakeMediator { +CreateOrderHandler() })

spy.send(CreateOrderCommand(id = "ORD-1"))
spy.assertSentCount<CreateOrderCommand>(1)

spy.reset()

spy.send(CreateOrderCommand(id = "ORD-2"))
spy.assertSentCount<CreateOrderCommand>(1) // count starts fresh after reset
}

Choosing the right helper

SituationUse
Test only checks initial state, never calls sendDummyMediator
Test controls what send returnsFakeMediator + fakeHandler
Test asserts which requests were sentMediatorSpy
Test captures published notificationscaptureNotifications or MediatorSpy
Test verifies all handlers are wired upMediatorTestUtils.assertAllHandlersRegistered

Next

Handler Validation