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:

  1. when engine starts successfully, verify that our instrument panel indicates the car is ready to drive.

  2. 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() }
  }
}
Previous
Previous

StateFlow and SharedFlow in ViewModels

Next
Next

Compose by example: BoxWithConstraints