1. Introduction

Mocking is an essential aspect of unit testing. It allows us to isolate components and ensure that each part of our codebase functions as expected. In Kotlin, a powerful testing framework called MockK provides a feature known as spies. Spying is an advanced use case where we can watch real objects with the mocking framework.

In this tutorial, we’ll explore the concept of spies in MockK and how they can enhance our unit testing process.

2. Understanding Mocking and Spies

Before we dive into spies, let’s briefly revisit the concept of mocking. In unit testing, mocking is creating fake objects that mimic the behavior of real objects. This allows developers to test components in isolation without relying on the actual implementation of dependent objects. We can create a mock with mockk():

val mock = mockk<Any>()

Spies, in the context of MockK, are a type of mock that allows partial mocking. This means we can use a spy to mock only specific methods of a real object while retaining the original behavior of the rest of the methods. This capability makes spies a versatile tool in unit testing, enabling developers to test certain aspects of their code without completely isolating the component being tested. We can create a spy with spyk():

val spy = spyk<Any>()

2.1. MockK Dependency

We’ll need to add MockK to our project to use these features. We can add it to our dependencies in the build.gradle or build.gradle.kts file:

3. Using Spies in Kotlin

Now, let’s explore how to use spies in Kotlin with MockK. Assume we have a simple class called Calculator with various mathematical operations:

fun add(a: Int, b: Int): Int {
    return a+b
}
fun findAverage(a: Int, b: Int): Int {
    val total = add(a,b)
    return total/2;
}

In this code, we define two functions: add() for adding two integers and findAverage(), which uses the add() function to calculate the average of two integers.

We use the spyk() function to create a spy instance of our Calculator. This spy allows us to observe, intercept, and verify method calls, gaining insights into how the methods are being invoked during the test:

class CalculatorTest {
    @Test
    fun testSpy() {
        val spy = spyk<Calculator>()
        val result = spy.findAverage(5, 5)
        verify { spy.add(5, 5) }
        assertEquals(5, result)
    }
}

In this code, our test uses a spyk() to observe the behavior of the Calculator. Specifically, the verify() line ensures that the add() method is called with the expected arguments, allowing us to validate the interactions with the spy and verify interactions with the spy.

4. Partial Mocking with Spies

In addition to observing and verifying method calls, MockK allows us to partially mock methods on a spy. Consider our previous example and demonstrate how to mock the add() method on the Calculator spy to return a specific value. This helps when we want to control the behavior of certain methods while still executing the real implementation of others:

@Test
fun testPartialMocking() {
    val spy = spyk<Calculator>()
    every { spy.add(any(), any()) } returns 2
    val result = spy.findAverage(5, 5)
    verify { spy.add(5, 5) }
    assertEquals(1, result)
}

In this example, we can control our spy by passing a function to every(). We can specify specific arguments, but for simplicity’s sake, this time, we picked any() arguments. Finally, we can configure the returns() value for the spy, which is two in this case.

This partial mocking allows us to control the behavior of the add() method while still executing the real implementation of the findAverage() method. The test then verifies that the add() method was called with the expected arguments, and the result of findAverage() reflects the real behavior with the mocked addition.

5. Resetting Spies

Finally, we can reset both mocked behavior and calls recording on a spy with clearMocks():

@Test
fun testPartialMocking() {
    val spy = spyk<Calculator>()
    every { spy.add(any(), any()) } returns 2
    val result = spy.findAverage(5, 5)
    verify { spy.add(5, 5) }
    assertEquals(1, result)
    clearMocks(spy)
}

The use of clearMocks() will reset any behavior configured with every() on the spy. This will also reset any recorded calls we check with verify().

6. Benefits of Using Spies

Spies allow us to mock specific methods while preserving the original behavior of the rest. This is particularly useful when we want to focus on testing a specific part of a class without completely isolating it.

Unlike regular mocks, spies let us invoke the real methods of the object being spied on. This is valuable when we want to test the integration of real methods with the mocked ones on the same object.

Spies can enhance code maintainability by allowing developers to write more focused and concise tests. When using spies, we can choose which methods to mock and which to keep real, resulting in cleaner and more readable test code.

7. Conclusion

In the realm of unit testing with Kotlin, spies offer developers a flexible and powerful tool. They enable partial mocking, allowing us to test specific components of our code while maintaining the original behavior of the rest. By incorporating spies into our testing strategy, we can strike a balance between isolating code for testing and ensuring realistic interactions between different components.

As always, the sample code presented is available on GitHub.