1. Overview
In this tutorial, we’re going to take a look at some of the basic features of the MockK library.
2. MockK
In Kotlin, all classes and methods are final. While this helps us write immutable code, it also causes some problems during testing.
Most JVM mock libraries have problems with mocking or stubbing final classes. Of course, we can add the “open” keyword to classes and methods that we want to mock. But changing classes only for mocking some code doesn’t feel like the best approach.
Here comes the MockK library, which offers support for Kotlin language features and constructs. MockK builds proxies for mocked classes. This causes some performance degradation, but the overall benefits that MockK gives us are worth it.
3. Installation
Installation is as simple as it can be. We just need to add the mockk dependency to our Maven project:
<dependency> <groupId>io.mockk</groupId> <artifactId>mockk</artifactId> <version>1.9.3</version> <scope>test</scope> </dependency>
For Gradle, we need to add it as a test dependency:
testImplementation "io.mockk:mockk:1.9.3"
4. Basic Example
Let’s create a service that we’d like to mock:
class TestableService { fun getDataFromDb(testParameter: String): String { // query database and return matching value } fun doSomethingElse(testParameter: String): String { return "I don't want to!" } }
Here’s an example test that mocks TestableService:
@Test fun givenServiceMock_whenCallingMockedMethod_thenCorrectlyVerified() { // given val service = mockk<TestableService>() every { service.getDataFromDb("Expected Param") } returns "Expected Output" // when val result = service.getDataFromDb("Expected Param") // then verify { service.getDataFromDb("Expected Param") } assertEquals("Expected Output", result) }
To define the mock object, we’ve used the mockk<…>() method.
In the next step, we defined the behavior of our mock. For this purpose, we’ve created an every block that describes what response should be returned for which call.
Finally, we used the verify block to verify whether the mock was invoked as we expected.
5. Annotation Example
It is possible to use MockK annotations to create all kind of mocks. Let’s create a service that requires two instances of our TestableService:
class InjectTestService { lateinit var service1: TestableService lateinit var service2: TestableService fun invokeService1(): String { return service1.getDataFromDb("Test Param") } }
InjectTestService contains two fields with the same type. It won’t be a problem for MockK. MockK tries to match properties by name, then by class or superclass. It also has no problem with injecting objects into private fields.
Let’s mock InjectTestService in a test by using annotations:
class AnnotationMockKUnitTest { @MockK lateinit var service1: TestableService @MockK lateinit var service2: TestableService @InjectMockKs var objectUnderTest = InjectTestService() @BeforeEach fun setUp() = MockKAnnotations.init(this) // Tests here ... }
In the above example, we’ve used the @InjectMockKs annotation. This specifies an object where defined mocks should be injected. By default, it injects variables that are not assigned yet. We can use @OverrideMockKs to override fields that have a value already.
MockK requires MockKAnnotations.init(…) to be called on an object declaring a variable with annotations. For Junit5, it can be replaced with @ExtendWith(MockKExtension::class).
6. Spy
Spy allows mocking only a particular part of some class. For example, it can be used to mock a specific method in TestableService:
@Test fun givenServiceSpy_whenMockingOnlyOneMethod_thenOtherMethodsShouldBehaveAsOriginalObject() { // given val service = spyk<TestableService>() every { service.getDataFromDb(any()) } returns "Mocked Output" // when checking mocked method val firstResult = service.getDataFromDb("Any Param") // then assertEquals("Mocked Output", firstResult) // when checking not mocked method val secondResult = service.doSomethingElse("Any Param") // then assertEquals("I don't want to!", secondResult) }
In the example, we’ve used the spyk method to create a spy object. We could’ve also used the @SpyK annotation to achieve the same:
class SpyKUnitTest { @SpyK lateinit var service: TestableService // Tests here }
7. Relaxed Mock
A typical mocked object will throw MockKException if we try to call a method where the return value hasn’t been specified.
If we don’t want to describe the behavior of each method, then we can use a relaxed mock. This kind of mock provides default values for each function. For example, the String return type will return an empty String. Here’s a short example:
@Test fun givenRelaxedMock_whenCallingNotMockedMethod_thenReturnDefaultValue() { // given val service = mockk<TestableService>(relaxed = true) // when val result = service.getDataFromDb("Any Param") // then assertEquals("", result) }
In the example, we’ve used the mockk method with the relaxed attribute to create a relaxed mock object. We could’ve also used the @RelaxedMockK annotation:
class RelaxedMockKUnitTest { @RelaxedMockK lateinit var service: TestableService // Tests here }
8. Object Mock
Kotlin provides an easy way to declare a singleton by using the object keyword:
object TestableService { fun getDataFromDb(testParameter: String): String { // query database and return matching value } }
However, most of the mocking libraries have a problem with mocking Kotlin singleton instances. Because of this, MockK provides the mockkObject method. Let’s take a look:
@Test fun givenObject_whenMockingIt_thenMockedMethodShouldReturnProperValue(){ // given mockkObject(TestableService) // when calling not mocked method val firstResult = service.getDataFromDb("Any Param") // then return real response assertEquals(/* DB result */, firstResult) // when calling mocked method every { service.getDataFromDb(any()) } returns "Mocked Output" val secondResult = service.getDataFromDb("Any Param") // then return mocked response assertEquals("Mocked Output", secondResult) }
9. Hierarchical Mocking
Another useful feature of MockK is the ability to mock hierarchical objects. First, let’s create a hierarchical object structure:
class Foo { lateinit var name: String lateinit var bar: Bar } class Bar { lateinit var nickname: String }
The Foo class contains a field of type Bar. Now, we can mock the structure in just one easy step. Let’s mock the name and nickname fields:
@Test fun givenHierarchicalClass_whenMockingIt_thenReturnProperValue() { // given val foo = mockk<Foo> { every { name } returns "Karol" every { bar } returns mockk { every { nickname } returns "Tomato" } } // when val name = foo.name val nickname = foo.bar.nickname // then assertEquals("Karol", name) assertEquals("Tomato", nickname) }
10. Capturing Parameters
If we need to capture the parameters passed to a method, then we can use CapturingSlot or MutableList. It is useful when we want to have some custom logic in an answer block or we just need to verify the value of the arguments passed. Here is an example of CapturingSlot:
@Test fun givenMock_whenCapturingParamValue_thenProperValueShouldBeCaptured() { // given val service = mockk<TestableService>() val slot = slot<String>() every { service.getDataFromDb(capture(slot)) } returns "Expected Output" // when service.getDataFromDb("Expected Param") // then assertEquals("Expected Param", slot.captured) }
MutableList can be used to capture and store specific argument values for all method invocations:
@Test fun givenMock_whenCapturingParamsValues_thenProperValuesShouldBeCaptured() { // given val service = mockk<TestableService>() val list = mutableListOf<String>() every { service.getDataFromDb(capture(list)) } returns "Expected Output" // when service.getDataFromDb("Expected Param 1") service.getDataFromDb("Expected Param 2") // then assertEquals(2, list.size) assertEquals("Expected Param 1", list[0]) assertEquals("Expected Param 2", list[1]) }
11. Conclusion
In this article, we’ve discussed the most important features of MockK. MockK is a powerful library for the Kotlin language and provides a lot of useful features. For more information about MockK, we can check the documentation on the MockK website.
As always, the sample code presented is available over on over on GitHub.