Introduction to MockK, Part One
After we have finished this webinar, we:
- Know what MockK is.
- Can get the required dependencies with Maven and Gradle.
- Know how we can create test doubles with MockK.
- Understand how we can integrate MockK with JUnit 5.
This webinar assumes that:
- You can run Kotlin tests with Maven or Gradle
- You can write test classes with JUnit 5 and Kotlin
Get the source code from Github
What Is MockK?
MockK is a mocking library for Kotlin. It’s key features are:
- It supports mocks, stubs, and spies.
- It supports both strict and relaxed stubs.
- It has a very versatile support for argument matching.
- We can create our test doubles by using either the manual approach or annotations.
- It provides a built-in support for both JUnit 5 and Spring Boot.
Next, we will find out how we can get the required dependencies with Maven and Gradle.
Getting the Required Dependencies
If we are using Kotlin 1.3.0 and Coroutines 1.0 (or newer), we have to declare the mockk 1.9.3 dependency in our build script.
If we are using Maven, we can get this dependency by adding the following dependency declaration to the dependencies section of our POM file:
1<dependency>
2 <groupId>io.mockk</groupId>
3 <artifactId>mockk</artifactId>
4 <version>1.9.3</version>
5 <scope>test</scope>
6</dependency>
If we are using Gradle, we have to add the mockk dependency to the testImplementation dependency configuration. We can do this by adding the following line to the dependencies block of our build.gradle file:
1testImplementation('io.mockk:mockk:1.9.3')
Let’s move on and find out how we can create our test doubles by using the manual approach.
Creating Test Doubles With the Manual Approach
Before we continue, I want to point out that MockK doesn’t differentiate stubs and mocks. This means that if we want to create either a stub or a mock, we have to use the same function called mockk().
This is a bit annoying because the name of this function suggests that the created object is a mock. That being said, I use this function here because it makes things a bit more simple.
Next, we will create stubs, mocks, and Spies with MockK. Note that all the following examples replace a list with the created test double.
Creating Stubs
If we want to create a strict stub, we have to follow these steps:
- Add a
lateinitproperty to our test class and ensure that this property contains a list ofLongobjects. - Add a setup function to our test class and ensure that this function is invoked before a test function is run. We will create our stub in this function.
- Create our stub by invoking the
mockk()function and store the created stub in thelistproperty. When we invoke themockk()function, we have to configure the type of the created stub.
After we have created a strict stub, the source code of our test class looks as follows:
1import io.mockk.mockk
2import org.junit.jupiter.api.BeforeEach
3import org.junit.jupiter.api.DisplayName
4import org.junit.jupiter.api.Tag
5
6@Tag("unitTest")
7@DisplayName("Demonstrates how we can create a strict stub")
8class StrictStubExampleTest {
9
10 private lateinit var list: List<Long>
11
12 @BeforeEach
13 fun createStub() {
14 list = mockk<List<Long>>()
15 }
16}
If we want to create a relaxed stub, we have to follow these steps:
- Add a
lateinitproperty to our test class and ensure that this property contains a list of objects. We have to use the star projection here because relaxed stubbing isn’t working properly with generic return types. - Add a setup function to our test class and ensure that this function is invoked before a test function is run. We will create our stub in this function.
- Create our stub by invoking the
mockk()function and store the created stub in thelistproperty. When we invoke themockk()function, we have to configure the type of the created stub and set the value of therelaxedfunction parameter totrue.
After we have created a relaxed stub, the source code of our test class looks as follows:
1import io.mockk.mockk
2import org.junit.jupiter.api.BeforeEach
3import org.junit.jupiter.api.DisplayName
4import org.junit.jupiter.api.Tag
5
6@Tag("unitTest")
7@DisplayName("Demonstrates how we can create a relaxed stub")
8class RelaxedStubExampleTest {
9
10 private lateinit var list: List<*>
11
12 @BeforeEach
13 fun createStub() {
14 list = mockk<List<*>>(relaxed = true)
15 }
16}
Additional Reading:
Let’s move on and find out how we can create mocks with MockK.
Creating Mocks
If we want to create a strict mock, we have to follow these steps:
- Add a
lateinitproperty to our test class and ensure that this property contains a list ofLongobjects. - Add a setup function to our test class and ensure that this function is invoked before a test function is run. We will create our mock in this function.
- Create our mock by invoking the
mockk()function and store the created mock in thelistproperty. When we invoke themockk()function, we have to configure the type of the created mock.
After we have created a strict mock, the source code of our test class looks as follows:
1import io.mockk.mockk
2import org.junit.jupiter.api.BeforeEach
3import org.junit.jupiter.api.DisplayName
4import org.junit.jupiter.api.Tag
5
6@Tag("unitTest")
7@DisplayName("Demonstrates how we can create a strict mock")
8class StrictMockExampleTest {
9
10 private lateinit var list: List<Long>
11
12 @BeforeEach
13 fun createMock() {
14 list = mockk<List<Long>>()
15 }
16}
If we want to create a relaxed mock, we have to follow these steps:
- Add a
lateinitproperty to our test class and ensure that this property contains a list ofLongobjects. Note that we don’t have to use the star projection here as long as we don’t stub the return values of the invoked functions. - Add a setup function to our test class and ensure that this function is invoked before a test function is run. We will create our mock in this function.
- Create our mock by invoking the
mockk()function and store the created mock in thelistproperty. When we invoke themockk()function, we have to configure the type of the created mock and set the value of therelaxedfunction parameter totrue.
After we have created a relaxed mock, the source code of our test class looks as follows:
1import io.mockk.mockk
2import org.junit.jupiter.api.BeforeEach
3import org.junit.jupiter.api.DisplayName
4import org.junit.jupiter.api.Tag
5
6@Tag("unitTest")
7@DisplayName("Demonstrates how we can create a relaxed mock")
8class RelaxedMockExampleTest {
9
10 private lateinit var list: List<Long>
11
12 @BeforeEach
13 fun createMock() {
14 list = mockk<List<Long>>(relaxed = true)
15 }
16}
If we want to mock or stub Kotlin objects, we have to follow these steps:
- Add a setup function to our test class and ensure that this function is invoked before a test function is run. We will create our mock or stub in this function.
- Replace the Kotlin object with the our test double. We can do this by invoking the
mockkObject()function. When we invoke this function, we must specify the Kotlin object that’s replaced with our test double. - Add a teardown function to our test class and ensure that this function is invoked after a test function has been run. We will destroy our test double in this function.
- Destroy our test double by invoking the
unmockkObject()function. When we invoke this function, we must specify the Kotlin object that was replaced with a test double.
After we have replaced a Calculator object with a mock, the source code of our test class looks as follows:
1import io.mockk.mockkObject
2import io.mockk.unmockkObject
3import org.junit.jupiter.api.AfterEach
4import org.junit.jupiter.api.BeforeEach
5import org.junit.jupiter.api.DisplayName
6import org.junit.jupiter.api.Tag
7
8@Tag("unitTest")
9@DisplayName("Demonstrates how we can mock Kotlin objects")
10class ObjectMockExampleTest {
11
12 @BeforeEach
13 fun createMock() {
14 mockkObject(Calculator)
15 }
16
17 @AfterEach
18 fun destroyMock() {
19 unmockkObject(Calculator)
20 }
21}
There are two things I want to point out:
- We can use the this technique if we want to replace a Kotlin object with a stub.
- If we have replaced multiple Kotlin objects with a test double, we can also destroy our test doubles by invoking the
unmockkAll()function.
Additional Reading:
Next, we will learn to create spies with MockK.
Creating Spies
If we want to create a spy, we have to follow these steps:
- Add a
lateinitproperty to our test class and ensure that this property contains a mutable list ofLongobjects. - Add a setup function to our test class and ensure that this function is invoked before a test function is run. We will create our spy in this function.
- Create our spy by invoking the
spyk()function and store the created spy in thelistproperty. When we invoke thespyk()function, we have to configure the type of the created spy.
After we have created a spy, the source code of our test class looks as follows:
1import io.mockk.spyk
2import org.junit.jupiter.api.BeforeEach
3import org.junit.jupiter.api.DisplayName
4import org.junit.jupiter.api.Tag
5
6@Tag("unitTest")
7@DisplayName("Demonstrates how we can create a spy")
8class SpyExampleTest {
9
10 private lateinit var list: MutableList<Long>
11
12 @BeforeEach
13 fun createSpy() {
14 list = spyk<ArrayList<Long>>()
15 }
16}
Additional Reading:
We can now create different test doubles by using the manual approach. Let’s move on and find out how we can create our test doubles by using MockK annotations.
Creating Test Doubles With Annotations
When we want to create test doubles by using the MockK annotations, we can use the following annotations:
- The
@MockKannotation allows us to create both strict and relaxed stubs and mocks. If we want to create a relaxed stub or a mock, we have to set the value of the@MockKannotation’srelaxedattribute totrue. - The
@RelaxedMockKannotation allows us to create relaxed stubs and mocks. - The
@SpyKannotation allows us to create spies.
Next, we will create different test doubles by using these annotations. We can create our test doubles by following these steps:
- Add a
lateinitproperty to our test class and ensure that this property contains a strict mock. - Add a
lateinitproperty to our test class and ensure that this property contains a relaxed mock. - Add a new property to our test class and ensure that this property contains a spy.
- Add a
lateinitproperty to our test class and ensure that this property contains a strict stub. - Add a
lateinitproperty to our test class and ensure that this property contains a relaxed stub. - Add a setup function to our test class and ensure this function is invoked before a test function is run. We will create our test doubles in this function.
- Create our test doubles by invoking the
static init()function of theMockKAnnotationsclass. When we invoke this function, we must configure the test object that contains the fields or properties which store our test doubles.
After we have written a test class that creates our test doubles with MockK annotations, its source code looks as follows:
1import io.mockk.MockKAnnotations
2import io.mockk.impl.annotations.MockK
3import io.mockk.impl.annotations.RelaxedMockK
4import io.mockk.impl.annotations.SpyK
5import org.junit.jupiter.api.BeforeEach
6import org.junit.jupiter.api.DisplayName
7
8@DisplayName("Demonstrates how we can create test doubles with annotations")
9class AnnotationTest {
10
11 @MockK
12 private lateinit var mockList: List<Long>
13
14 @MockK(relaxed = true)
15 private lateinit var relaxedMockList: List<Long>
16
17 @SpyK
18 private var spyList = ArrayList<Long>()
19
20 @MockK
21 private lateinit var stubList: List<Long>
22
23 @RelaxedMockK
24 private lateinit var relaxedStubList: List<*>
25
26 @BeforeEach
27 fun createTestDoubles() {
28 MockKAnnotations.init(this)
29 }
30}
Before we continue, I want to point out that I am not a huge fan of using annotation for this purpose because they make our life too easy. The developers of different mocking libraries added annotation support to their libraries because the users of their libraries were complaining that creating test doubles requires too much code.
The problem is that annotations help us to hide the design problems found from our code. I think that if configuring the system under test is too cumbersome, the system under test has too many dependencies and we must refactor it. That’s why I think that we shouldn’t create our test double with annotations.
Additional Reading:
Next, we will find out how we can create our test doubles by using annotations and JUnit 5.
Integrating MockK With JUnit 5
If we are using JUnit 5, and we want to create our test doubles by using MockK annotations, we can create our test doubles by using the MockKExtension. When we want to use this extension, we have to make the following changes to the test class which created during the previous section:
- Remove the setup function from our test class.
- Ensure that the
MockKExtensionis invoked when we run the tests found from our test class
After we have made these changes to our test class, its source code looks as follows:
1import io.mockk.impl.annotations.MockK
2import io.mockk.impl.annotations.RelaxedMockK
3import io.mockk.impl.annotations.SpyK
4import io.mockk.junit5.MockKExtension
5import org.junit.jupiter.api.DisplayName
6import org.junit.jupiter.api.extension.ExtendWith
7
8@ExtendWith(MockKExtension::class)
9@DisplayName("Demonstrates how we can integrate mockk with junit 5")
10class JUnit5Test {
11
12 @MockK
13 private lateinit var mockList: List<Long>
14
15 @MockK(relaxed = true)
16 private lateinit var relaxedMockList: List<Long>
17
18 @SpyK
19 private var spyList = ArrayList<Long>()
20
21 @MockK
22 private lateinit var stubList: List<Long>
23
24 @RelaxedMockK
25 private lateinit var relaxedStubList: List<*>
26}
Additional Reading:
We can now create our test doubles with MockK and we know how we can integrate it with JUnit 5. Let’s summarize what we learned from this webinar.
Summary
This webinar has taught us four things:
- We can create mocks and stubs by using either the
mockk()function,@MockKannotation, or the@RelaxedMockKannotation. - If we want mock Kotlin objects, we have to use the
mockkObject()andunmockkObject()functions. - We can create spies by using either the
spyk()function or the@SpyKannotation. - If we want to create our test doubles with annotations, we have to initialize our test doubles by using either the
static init()function of theMockKAnnotationsclass or theMockKExtensionclass.