Kotlin Unit Testing guide — part 2 — mocking dependencies

Kacper Wojciechowski
12 min readJul 26, 2024

--

In previous episode, I covered the basics of Unit Testing. Our test subject was a simple class with a single function and no dependencies. In the real world, classes are interconnected, often using constructor injection. In this episode, we’ll explore how to handle these dependencies in unit testing.

Why mocking?

To construct our test subject, we need instances of all its dependencies. Using real implementations of these dependencies is often impractical due to their complexity and potential platform dependencies, which may not work in Unit Tests. Moreover, it would violate the primary principle of unit testing, which is to test only the unit, the test subject.

If you’re writing clean code, you probably use interfaces to hide implementations. This allows you to create fake implementations of those interfaces. However, while this makes Unit Tests more performant, it generates significant boilerplate and has limited features that you need to implement yourself.

Mocking to the rescue

Mocking frameworks create instances for you, which aren’t the actual implementations but objects reflecting the type signature of the dependency (usually through reflection). This maximizes the isolation of the test subject. Mocking frameworks also provide two core features necessary for writing good unit tests:

  • Mocking dependency functions and property behaviour
  • Verifying the usage of those dependencies

Let’s start mocking!

Start by picking the mocking framework:
1. Mockk
2. Mockito

Mockito is an old-school Java-based framework rarely used in Kotlin codebases. I prefer Mockk for its Kotlin-friendly syntax and support for coroutines and Kotlin objects.

[versions]
test-mockk = "1.13.12"

[libraries]
test-mockk = { module = "io.mockk:mockk", version.ref = "test-mockk" }
testImplementation(libs.test.mockk)

Simple mocking

Our test subject ExampleUseCase now has a dependency providing the multiplier:

class ExampleDependency {
fun provideMultiplier(): Int = 2
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke(input: Int): Int {
return input
.times(exampleDependency.provideMultiplier())
.coerceAtMost(4)
}
}

Let’s go back to our test class. Now our test subject cannot be simply instantiated as it requires an object in its constructor. We need to mock this dependency:

class ExampleUseCaseTest {

private val exampleDependency = mockk<ExampleDependency>()

private val exampleUseCase = ExampleUseCase(
exampleDependency = exampleDependency,
)
}

The mockk function will create a mock object that pretends to be ExampleDependency through reflection. Now let’s create a test where the multiplier is 2 and input is 1.

@Test
fun `invoke returns multiplication result when multiplier is 2 and input is 1`() {
val result = exampleUseCase(input = 1)

assertEquals(2, result)
}

But what happens when we run this test? We will get an exception:

io.mockk.MockKException: no answer found for ExampleDependency(#1).provideMultiplier() among the configured answers

This is because we actually need to tell the mock to behave the way we want it to. To do so we need to use configuration functions. In mockk to do so we use every function that will tell the mockk framework which call we want to configure. Then we use functions as building blocks to tell how we want it to behave. In our case we need to use returns infix to return 2. This is a configuration of a mock, so it should land in the GIVEN block:

@Test
fun `invoke returns multiplication result when multiplier is 2 and input is 1`() {
every { exampleDependency.provideMultiplier() } returns 2

val result = exampleUseCase(input = 1)

assertEquals(2, result)
}

Mocking also allows verifying that the function was called. This is useful to ensure the class is used at the designated moment or under certain conditions.

To do so we use verify functions. There are multiple variants of them:

  • verify — Confirms that a specified call occurred at least once, regardless of order or frequency.
  • verifyOrder — Ensures that the specified calls occurred in the given order at least once, but other calls in between are allowed.
  • verifySequence —Verifies that the specified calls occurred in the exact order provided and that no other calls to the mocked class occurred.

Of course this is a verification of the test result, so it should land in the THEN block.

@Test
fun `invoke returns multiplication result when multiplier is 2 and input is 1`() {
every { exampleDependency.provideMultiplier() } returns 2

val result = exampleUseCase(input = 1)

assertEquals(2, result)
verifySequence {
exampleDependency.provideMultiplier()
}
}

Mock function parameters

In the previous example, the mock dependency function did not take any parameters. In real-world scenarios, functions often take parameters, and we need to handle them correctly. Within the every lambda, you need to specify these parameters for the mocking framework to properly match and find the answers. You can pass the actual values you expect or use helper functions provided by MockKMatcherScope inside the every and verify functions. While there are many situational helpers, the general rule of thumb is:

  • In every calls, use any() to ensure the MockK framework can always find your answer and won't throw a confusing "no answer found" exception.
  • In verify calls, use actual values to verify that the expected calls happened with the expected parameters. This makes the exceptions more understandable, which is why I always use any() in every calls.

Let’s write a test for this example use case. It fetches some item from some dependency and returns it:

class ExampleDependency {
fun getItem(id: Int): String = "some item"
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke(input: Int) = exampleDependency.getItem(input)
}

We need to mock the getItem function and verify that the use case called this dependency with expected parameter:

@Test
fun `invoke fetches item from dependency and returns it`() {
every { exampleDependency.getItem(any()) } returns "item"

val result = exampleUseCase(input = 1)

assertEquals("item", result)
verifySequence {
exampleDependency.getItem(id = 1)
}
}

Configuring mocks to return multiple different values

Sometimes, you might need a mock to return one value initially and then switch to another. This is particularly useful when testing retry mechanisms. Let’s look at an example:

interface ExampleDependency {
fun fetch(): Result<Unit>
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke() {
exampleDependency.fetch()
.onFailure {
invoke()
}
}
}

In a real-world scenario, there would be a safety mechanism to prevent an infinite loop, but for the sake of this example, we will test what happens when the dependency fetch first fails and then succeeds. Instead of using the returns infix, we will use the returnsMany function:

@Test
fun `invoke should retry to fetch in case of failure`() {
every { exampleDependency.fetch() }.returnsMany(
Result.failure(Exception()),
Result.success(Unit),
)

exampleUseCase()

verifySequence {
exampleDependency.fetch()
exampleDependency.fetch()
}
}

This way we configure the mock to return failure when invoked for the first time, and then switch to success when invoked next time.

Suspending functions

Some functions we need to test use the suspend keyword, requiring them to be in a coroutine scope. Mockk provides straightforward support for coroutines. Simply add the co prefix to the configuration or verification function you want to use. For example, let's say the getItem function from the previous example uses coroutines:

class ExampleDependency {
suspend fun getItem(id: Int): String = "some item"
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke(input: Int) =
runBlocking { exampleDependency.getItem(input) }
}

For now I’m using runBlocking as the coroutines testing will be explained in the next episode of the guide!

To write a unit test for that, simply add the co prefix to both the every and verify functions:

@Test
fun `invoke fetches item from dependency and returns it`() {
coEvery { exampleDependency.getItem(any()) } returns "item"

val result = exampleUseCase(input = 1)

assertEquals("item", result)
coVerifySequence {
exampleDependency.getItem(id = 1)
}
}

Verify that the call did not happen

In some cases, you might want to ensure that a call to a dependency did not occur. For example, if an event has already been sent, you might want to verify that the event is not sent a second time. There are two approaches to achieve this:

  • Use verify with exactly = 0: This is useful for cases where you want to verify that the call did not happen for specific parameters.
  • Use confirmVerified(*vararg mocks*): This verifies that the class was not used at all.
  • Use verifySequence: This will fail if there were any additional calls to this mock.
class ExampleDependency {
fun saveItem(id: Int) {}
fun alreadySaved(id: Int): Boolean = true
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke(input: Int) {
if (!exampleDependency.alreadySaved(input)) {
exampleDependency.saveItem(input)
}
}
}

In this case, we save an item only if it hasn’t been saved before. The dependency will be called continuously because we need to invoke alreadySaved for the if statement. Therefore, the most appropriate approach here is to use the third option:

@Test
fun `invoke does not save item as it was already saved`() {
every { exampleDependency.alreadySaved(any()) } returns true

exampleUseCase(input = 1)

verifySequence {
exampleDependency.alreadySaved(id = 1)
}
}

If you remove the if block, the test will fail because there will be an unexpected additional call to the mock that isn't accounted for in the verifySequence function.

Unit functions in mocks

Some functions have a return type of Unit (e.g., functions that perform an action without returning a value). You might think that you need to mock such functions with:

every { exampleDependency.saveItem(any()) } returns Unit

This approach works, but there’s a more elegant way. MockK provides infix functions specifically for this purpose:

  • just Runs for non-suspend functions
  • just Awaits for suspend functions
every { exampleDependency.saveItem(any()) } just Runs
coEvery { exampleDependency.saveItem(any()) } just Awaits

However, there’s an even simpler approach. If your class contains one or more functions that return Unit, you can configure them all at once by using the relaxUnitFun argument when creating the mock:

private val exampleDependency = mockk<ExampleDependency>(relaxUnitFun = true)

The relaxUnitFun argument tells mock to automatically handle all Unit functions without explicitly specifying their behaviour.

Similarly, the relaxed argument can be used to configure all functions that return simple types (e.g., String, Int). However, be cautious with this feature, as it may introduce unexpected behaviour. The mock might return default values that you assume are properly configured, potentially leading to confusing test results.

Reusable mock configuration

In certain scenarios, you might want to reuse mock configurations across multiple test cases. For JUnit 4 users, a common approach is to use the @Before method to set up the mock before each test case. However, Mockk offers a more streamlined solution by allowing you to specify default configurations directly during mock creation.

private val exampleDependency = mockk<ExampleDependency> {
every { alreadySaved(any()) } returns true
}

Mocking objects and static functions

Mocking objects and static functions is generally discouraged because it often indicates a design issue. Instead of relying on static functions or objects, consider refactoring your code to wrap these static calls in a class that can be injected. This approach makes your code more testable and maintainable.

However, there are cases where you need to mock Kotlin objects or Java static functions directly. Let’s look at an example:

object UserHelper {
fun generateRandomUserID() = UUID.randomUUID().toString()
}

interface ExampleDependency {
fun saveUser(id: String)
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke() {
exampleDependency.saveUser(UserHelper.generateRandomUserID())
}
}

To test the ExampleUseCase class, you need to mock UserHelper.generateRandomUserID() to control the user ID generated. In order to do so we need to use mockkObject function:

@Test
fun `invoke should generate user id and save user`() {
mockkObject(UserHelper)
every { UserHelper.generateRandomUserID() } returns "user id"

exampleUseCase()

coVerifySequence {
exampleDependency.saveUser("user id")
}
unmockkObject(UserHelper)
}

Important: Always use unmockkObject() at the end of your test to clean up. Static mocking persists across all tests, and failing to unmock it can lead to unpredictable test behaviour where some tests might pass when launched alone, but fail when launched all together!

Sometimes we are using Java static functions directly. Let’s use such an example:

interface ExampleDependency {
fun saveUser(id: String)
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke() {
exampleDependency.saveUser(UUID.randomUUID().toString())
}
}

In such case we need to mock UUID.randomUUID(). In order to mock a static function we need to pass its container class into mockkStatic function:

@Test
fun `invoke should generate user id and save user`() {
val uuid = UUID.fromString("c0a9035e-0b1a-485d-a5ff-f234977b04ff")
mockkStatic(UUID::class)
every { UUID.randomUUID() } returns uuid

exampleUseCase()

coVerifySequence {
exampleDependency.saveUser(uuid.toString())
}
unmockkStatic(UUID::class)
}

Mocking top level extension functions

Mocking extension functions is similar to mocking statics and objects. If you have to do this, then there is probably a way to inject a class with this logic instead of using a complex extension function. However, sometimes it is necessary, for example, to convert values into platform-specific classes. In such cases, we need to mock the extension function to perform the test, as we cannot use platform-specific classes that are not supported by plain JVM.

Mocking top-level extension functions requires some knowledge of how extension functions work under the hood. If you’ve ever decompiled Kotlin code, you know what I’m talking about. Kotlin extension functions are one of the killer features of Kotlin. You might wonder what amazing technology is used under the hood to make them work on the JVM. We are adding functionality to some object without extending the actual type. How is that possible? Well, extension functions are essentially static functions that take your extended object as the first parameter. Top-level functions are wrapped in a class with the name of the Kotlin file. For example, for an extension function inside a file named StringExtensions.kt:

fun String.sliceInHalf(): Pair<String, String> =
slice(0..(length / 2)) to slice((length / 2)..<length)

The underlying decompiled bytecode looks like this (I’m using this plugin to do so):

public final class StringExtensionsKt {
@NotNull
public static final Pair<String, String> sliceInHalf(@NotNull String $this$sliceInHalf) {
Intrinsics.checkNotNullParameter($this$sliceInHalf, "<this>");
return TuplesKt.to(StringsKt.slice($this$sliceInHalf, new IntRange(0, $this$sliceInHalf.length() / 2)), StringsKt.slice($this$sliceInHalf, RangesKt.until($this$sliceInHalf.length() / 2, $this$sliceInHalf.length())));
}
}

With this knowledge, we can test this class:

fun Bitmap.removeBlackColor(): Bitmap =
Bitmap.createBitmap(this).apply { eraseColor(0xFFFFFF) }

class ExampleUseCase {
operator fun invoke(bitmap: Bitmap) = bitmap.removeBlackColor()
}

Bitmap.removeBlackColor() is an extension function placed in the ExampleUseCase.kt file within the com.example package. Bitmap is an Android-specific type that cannot be directly used in a unit test, as plain JVM does not support it. To test this, we need to mock the extension function Bitmap.removeBlackColor(). We know that the extension function is a static method located inside a class named after the file. However, we cannot use mockkStatic as we did before, because we cannot access this class directly, as it is generated during compilation. To handle these cases, mockkStatic has an overload that accepts a String type, which should be the fully qualified name of the class. With this knowledge, we can write the following test case:

@Test
fun `invoke should create new bitmap with black color removed`() {
val originalBitmap = mockk<Bitmap>()
val modifiedBitmap = mockk<Bitmap>()
mockkStatic("com.example.ExampleUseCaseKt")
every { any<Bitmap>().removeBlackColor() } returns modifiedBitmap

val result = exampleUseCase(originalBitmap)

assertEquals(modifiedBitmap, result)
verifySequence {
originalBitmap.removeBlackColor()
}
unmockkStatic("com.example.ExampleUseCaseKt")
}

There is one catch that you need to remember. Inline functions cannot be mocked!

Catching mock function arguments

In some cases, what you actually want to verify is the object that your test subject is passing as a function argument. This is particularly useful when, for example, your test subject has a Flow that the function you want to test transforms and passes to another class (f.e. some click events flow). In such cases, you’ll want to verify the contents of this flow, but to do so, you need access to its instance. For this, you should use argument capturing mechanisms.

Let’s look at an example:

interface ExampleDependency {
fun register(flow: Flow<Unit>)
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
private val someEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1)

operator fun invoke() {
exampleDependency.register(someEvents.asSharedFlow())
}
}

In this case, we need to capture the Flow that is passed to the register(...) function. To do this, we create a Slot object of the desired type to act as a container for the captured argument. We then register this Slot using the capture function while configuring the register function with every. After invoking the corresponding function, the captured value can be accessed by calling slot.captured:

@Test
fun `invoke should register events flow in dependency`() {
val slot = slot<Flow<Unit>>()
every { exampleDependency.register(capture(slot)) } just Runs

exampleUseCase()

val flow = slot.captured
...
}

This way, you receive the instance of the Flow that was passed to your mock. You can then perform assertions and further tests on it.

Flow testing will be covered in a future episode, so I won’t expand on this topic here.

Partial mocking

There is one more technique that you might find useful, though it often indicates that the code could benefit from refactoring. In some cases, you might want to create a mock where only a subset of functions are mocked, while the rest use their real implementations. This can be achieved using a spy — a technique where you apply a layer of mocking over a real instance of a class. This allows you to mock and verify certain functions while delegating the rest of the functionality to the underlying real instance.

Here’s an example:

class ExampleDependency {
fun configureSmth() {}
fun getId(): String = UUID.randomUUID().toString()
}

class ExampleUseCase(
private val exampleDependency: ExampleDependency,
) {
operator fun invoke(): String {
exampleDependency.configureSmth()
return exampleDependency.getId()
}
}

If you have a situation where you don’t want the configureSmth() function to be mocked, you can create an instance of ExampleDependency and wrap it with the spyk() function. This allows you to mock specific functions while retaining the real behaviour for others:

private val exampleDependency = spyk(ExampleDependency())

...

@Test
fun `invoke should configure, get id and return it`() {
every { exampleDependency.getId() } returns "id"

val result = exampleUseCase()

assertEquals("id", result)
verifySequence {
exampleDependency.configureSmth()
exampleDependency.getId()
}
}

That’s all you need to know about mocking

In 95% of cases, simple mocking techniques will suffice, and often, the remaining 5% involve code that could benefit from refactoring to simplify testing. However, if you find yourself needing to understand every feature of mocking frameworks or dealing with particularly complex scenarios, the Mockk documentation provides comprehensive examples and details.

In the next part I’m covering the Kotlin Coroutines:

--

--