Testing functions with lambdas using MockK
December 18, 2022
Lambdas are common in Kotlin and MockK is the most popular mocking library for Kotlin nowadays. So at some point you will run into having to test functions with callback/lambda arguments. Below is an example of doing just that.
We are not going to compare various testing strategies like mocking, stubbing, etc. I will assume you are using mocking for this particular unit test.
Model
For the sake of simplicity, let’s assume that you construct a Car
with an Engine
and InstrumentPanel
only.
data class Car( private val engine: Engine, private val instrumentPanel: InstrumentPanel )
And here is more details about their behavior:
class Engine { fun start(onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { // TODO: start engine and invoke onSuccess if success or onFailure if failed } } class InstrumentPanel { fun readyToDrive() { // TODO: implement } fun checkEngine(throwable: Throwable) { // TODO: implement } } data class Car( private val engine: Engine, private val instrumentPanel: InstrumentPanel ) { fun startEngine() { engine.start( onSuccess = { instrumentPanel.readyToDrive() }, onFailure = { instrumentPanel.showCheckEngine(it) } ) } }
Testing
How do we test the Car
behavior using mocking with MockK? We’ll construct our test subject Car
by injecting Engine
and InstrumentPanel
mocks and verifying their behavior.
Given the API above, we will test 2 scenarios:
when engine starts successfully, verify that our instrument panel indicates the car is ready to drive.
when engine fails to start, verify that our instrument panel shows check engine light.
Mocking and capturing arguments
To do so, we can use an argument captor, io.mockk.slot()
for each of our lambda function arguments.
val onEngineStartSuccess = slot<() -> Unit>() val onEngineStartFailure = slot<(Throwable) -> Unit>()
In the case of our engine start success, the engine mock will answer by invoking the success callback.
every { engine.start( capture(onEngineStartSuccess), capture(onEngineStartFailure) ) } answers { onEngineStartSuccess.captured.invoke() }
In the case of our engine start failure, the engine mock will answer by invoking the failure callback
every { engine.start( capture(onEngineStartSuccess), capture(onEngineStartFailure) ) } answers { onEngineStartFailure.captured.invoke(engineException) }
Verifying behavior
Once the Engine
mock responds with those, we verify that correct functions on the instrument panel mock were invoked: readyToDrive()
and checkEngine(throwable)
respectively.
We will structure our tests with Given/When/Then
.
Putting it all together
import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.verify import org.junit.Test class CarTest { private val engine = mockk<Engine>() private val instrumentPanel = mockk<InstrumentPanel>() private val car = Car(engine, instrumentPanel) private val onEngineStartSuccess = slot<() -> Unit>() private val onEngineStartFailure = slot<(Throwable) -> Unit>() @Test fun `when engine starts, instrument panel indicates ready to drive`() { // Given every { engine.start( capture(onEngineStartSuccess), capture(onEngineStartFailure) ) } answers { onEngineStartSuccess.captured.invoke() } every { instrumentPanel.readyToDrive() } answers { nothing } // When car.startEngine() // Then verify { instrumentPanel.readyToDrive() } verify(exactly = 0) { instrumentPanel.checkEngine(any()) } } @Test fun `when engine fails to start, instrument panel shows check engine light`() { // Given val engineException = RuntimeException() every { engine.start( capture(onEngineStartSuccess), capture(onEngineStartFailure) ) } answers { onEngineStartFailure.captured.invoke(engineException) } every { instrumentPanel.checkEngine(any()) } answers { nothing } // When car.startEngine() // Then verify { instrumentPanel.checkEngine(engineException) } verify(exactly = 0) { instrumentPanel.readyToDrive() } } }