MockK is a mocking library which embraces Kotlin's unique features such as "final by default", objects, and coroutines. That's why we should use it if we have to write unit tests for a Kotlin project. In this first part of my MockK tutorial, we will learn how to create mocks with MockK and JUnit Jupiter.
After we have finished this blog post, we:
- Can get the required dependencies with Maven and Gradle.
- Understand how we can create mocks with MockK.
Let's begin.
Getting the Required Dependencies
Before we can write tests which use MockK, we have to choose between the standard MockK API and its BDD (Behavior-Driven Development) variant. Basically, we have to choose between the classic every/verify syntax and the BDD style given/then syntax. This choice defines the MockK module which we must add to the classpath.
The following table identifies the different modules provided by MockK:
| Build Tool | Test Style | Group ID | Artifact ID | Configuration / Scope |
|---|---|---|---|---|
| Gradle | Standard | io.mockk |
mockk |
testImplementation |
| Gradle | BDD | io.mockk |
mockk-bdd |
testImplementation |
| Maven | Standard | io.mockk |
mockk-jvm |
test |
| Maven | BDD | io.mockk |
mockk-bdd-jvm |
test |
Example 1: Gradle
If we want to use the standard MockK API with Gradle, we have to add the mockk dependency (version 1.14.9) to the testImplementation dependency configuration. We can do this by adding the following line to the dependencies block of our build.gradle.kts file:
testImplementation("io.mockk:mockk:1.14.9")
Example 2: Maven
If we want to use the standard MockK API with Maven, we have to add the mockk-jvm dependency (version 1.14.9) to the test scope. We can do this by adding the following dependency declaration to the dependencies section of our pom.xml file:
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk-jvm</artifactId>
<version>1.14.9</version>
<scope>test</scope>
</dependency>
- MockK is a Kotlin Multiplatform project that's published with Gradle module metadata.
- Because Gradle can read its own module metadata, it can resolve the correct variant automatically.
- Because Maven cannot read Gradle's module metadata, we have to explicitly specify the variant we want to use.
Next, we will take a closer look at the system under test.
Introduction to the System Under Test
During this blog post we will start writing tests for a component which allows us to register new user accounts. The implementation of this component is described in the following:
The User.kt file contains two data classes and one exception. These classes are:
- The
RegisterUserAccountRequestclass contains the information that's used to register a new user account. - The
UserAccountclass contains the information of a user account that's found from the database. - The
UserAccountExistsExceptionis a runtime exception that's thrown if the registered user account doesn't have a unique email address.
The source code of the User.kt file looks as follows:
data class RegisterUserAccountRequest(val email: String, val name: String)
data class UserAccount(val id: Long, val email: String, val name: String)
class UserAccountExistsException(email: String) :
RuntimeException("The user account with email address: $email exists")
The UserRepository interface declares two functions:
- The
existsByEmail()function returnstrueif the email address given as an argument is used by an existing user account andfalseotherwise. - The
save()function saves the user account provided as an argument and returns the information that's inserted into the database.
The source code of the UserRepository interface looks as follows:
interface UserRepository {
fun existsByEmail(email: String): Boolean
fun save(input: RegisterUserAccountRequest): UserAccount
}
The EmailService interface declares one function that's used to send a welcome email to a new user after their user account has been registered. The source code of the EmailService interface looks as follows:
interface EmailService {
fun sendWelcomeEmail(email: String)
}
The UserAccountRegistrationService class has one function called registerUserAccount() that's implemented by following these steps:
- Throw the
UserAccountExistsExceptionif the registered user account doesn't have a unique email address. - Insert a new user account into the database.
- Send the welcome email to the user.
- Return the information that was inserted into the database.
The source code of the UserAccountRegistrationService class looks as follows:
class UserAccountRegistrationService(
private val repository: UserRepository,
private val emailService: EmailService
) {
fun registerUserAccount(input: RegisterUserAccountRequest): UserAccount {
if (repository.existsByEmail(input.email)) {
throw UserAccountExistsException(input.email)
}
val registeredUserAccount = repository.save(input)
emailService.sendWelcomeEmail(registeredUserAccount.email)
return registeredUserAccount
}
}
Let's move on and find out how we can create mocks with MockK and configure the system under test with JUnit Jupiter.
Creating Mocks With MockK
When we are writing our tests with JUnit Jupiter and we want to create mocks with MockK, we have two viable options:
- We can create a mock manually by invoking the
mockk()function. - We can use the
MockKExtensiontogether with the@MockKand@InjectMockKsannotations.
Let's start and find out how we can create mocks manually by using the @BeforeEach function.
Creating Mocks Manually by Using the @BeforeEach Function
When we want to replace the UserRepository and the EmailService dependencies of the UserAccountRegistrationService class with mocks by using the @BeforeEach function, we have to follow these steps:
1. Create a new test class and add three private lateinit properties to the created test class:
- The
repositoryproperty contains theUserRepositorymock. - The
emailServiceproperty contains theEmailServicemock. - The
serviceproperty contains the system under test (aUserAccountRegistrationServiceobject).
After we have added these properties to our test class, its source code looks as follows:
class UserAccountRegistrationServiceManualTest {
private lateinit var repository: UserRepository
private lateinit var emailService: EmailService
private lateinit var service: UserAccountRegistrationService
}
2. Add a setup function to our test class and ensure that this function is invoked before a test function is run. After we have added a new setup function to our test class, its source code looks as follows:
import org.junit.jupiter.api.BeforeEach
class UserAccountRegistrationServiceManualTest {
private lateinit var repository: UserRepository
private lateinit var emailService: EmailService
private lateinit var service: UserAccountRegistrationService
@BeforeEach
fun configureSystemUnderTest() {
}
}
3. Implement the setup function by following these steps:
- Create new
UserRepositoryandEmailServicemocks by invoking themockk()function and store the created mocks in therepositoryandemailServiceproperties. - Create a new
UserAccountRegistrationServiceobject and store the created object in theserviceproperty.
After we have implemented the setup function, the source code of our test class looks as follows:
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
class UserAccountRegistrationServiceManualTest {
private lateinit var repository: UserRepository
private lateinit var emailService: EmailService
private lateinit var service: UserAccountRegistrationService
@BeforeEach
fun configureSystemUnderTest() {
repository = mockk()
emailService = mockk()
service = UserAccountRegistrationService(repository, emailService)
}
}
We have now configured the system under test by using the @BeforeEach function. Let's take a look at the pros and cons of this approach:
Pros:
- Tests are isolated. Because every test function gets new mocks and a new system under test, there is no risk of "configuration leakage". In other words, stubbed functions and verifications cannot leak from one test to another.
- Doesn't require a specific test instance lifecycle mode. If we create the required mocks and the system under test in the
@BeforeEachfunction, our test functions are isolated in every supported test instance lifecycle mode of JUnit Jupiter. For example, because we create new objects and assign property values before every test function, our configuration guarantees a clean slate even if JUnit Jupiter is reusing the test class instance. - Explicit dependency injection. Because we create the system under test manually in the
@BeforeEachfunction, we don't have to worry about the reflection magic that's used if we configure the system under test by using annotations. - Can be used as a design tool. Because we have to manually create the system under test, we will feel the pain immediately if the system under test has too many dependencies. If it feels cumbersome to write the code that configures the system under test or the constructor call becomes so long that it's hard to read, we should refactor the system under test.
Cons:
- Requires nullable or lateinit properties. Because the properties are initialized in a
@BeforeEachfunction and not in a test class constructor, we are forced to use nullable types or lateinit properties. This is a problem because:- If we use a lateinit property: we must use
varinstead ofvaland we cannot rely on compile-time null safety. In other words, the property can be accidentally reassigned outside of the@BeforeEachfunction, or we might simply forget to assign its value. The problem is that instead of a compiler error, we get a runtime error during test execution. - If we use a nullable type: we have to pay the "null-safety" tax (either use the "bang-bang" operator (
!!) or do safe calls (?.)), we have to provide a default value (null) that's immediately replaced either with a mock or the tested object, and we mask the intent of the property because in Kotlin a nullable type typically means that the value can be missing which isn't the case here.
- If we use a lateinit property: we must use
- Requires boilerplate code. We have to write the setup code manually for every test class. This means that our test suite will have setup code that isn't, strictly speaking, required.
- Requires maintenance. If we add a new dependency to the system under test, we have to add a new lateinit property to our test class, create a new mock object in the
@BeforeEachfunction, and modify the constructor call which creates the tested object.
Next, we will find how we can create mocks manually as immutable class properties.
Creating Mocks Manually as Immutable Class Properties
When we want to replace the UserRepository and the EmailService dependencies of the UserAccountRegistrationService class with mocks, and add these mocks and the system under test to our test class as immutable class properties, we have to create a new test class and follow these steps:
- Add a
privateand immutablerepositoryproperty to our test class and ensure that it contains aUserRepositorymock. - Add a
privateand immutableemailServiceproperty to our test class and ensure that it contains anEmailServicemock. - Add a
privateand immutableserviceproperty to our test class and ensure that it contains a newUserAccountRegistrationServiceobject. Remember to pass theUserRepositoryand theEmailServicemocks as constructor arguments.
After we have added the immutable class properties to our test class, its source code looks as follows:
import io.mockk.mockk
class UserAccountRegistrationServiceManualClassPropertyTest {
private val repository = mockk<UserRepository>()
private val emailService = mockk<EmailService>()
private val service = UserAccountRegistrationService(repository, emailService)
}
We have now configured the system under test by using immutable class properties. Let's take a look at the pros and cons of this approach.
Pros:
- All class properties are immutable. They cannot be reassigned and they cannot be
null. - Tests are isolated (if we use the correct test instance lifecycle mode). Because every test function gets new mocks and a new system under test, there is no risk of "configuration leakage". In other words, stubbed functions and verifications cannot leak from one test to another.
- Explicit dependency injection. Because we create the system under test manually, we don't have to worry about the reflection magic that's used if we configure the system under test by using annotations.
- Can be used as a design tool. Because we have to manually create the system under test, we will feel the pain immediately if the system under test has too many dependencies. If it feels cumbersome to write the code configures the system under test or the constructor call becomes so long that it's hard to read, we should refactor the system under test.
Cons:
- JUnit Jupiter must create a new test instance for every test function (this is the default behavior). As long as JUnit Jupiter creates a new test instance for every test function, there is no risk of "configuration leakage". If the JUnit Jupiter is configured the create a new test instance once per test class, a test function will inherit the stubs and recorded interactions from the previous test function (if we don't do manual cleanup). This shared state can lead to flaky tests where the outcome of one test depends on the execution order of the other tests.
- Requires boilerplate code. We have to write the setup code manually for every test class. This means that our test suite will have setup code that isn't, strictly speaking, required.
- Requires maintenance. If we add a new dependency to the system under test, we have to add a new immutable class property to our test class, create a new mock object, and modify the constructor call which creates the tested object.
Let's move on and find out how we can create mocks by using the JUnit Jupiter extension together with the @MockK and @InjectMockKs annotations.
Using the JUnit Jupiter Extension
When we want to replace the UserRepository and the EmailService dependencies of the UserAccountRegistrationService class with mocks by using MockK JUnit Jupiter extension, we have to follow these steps:
1. Create a new test class and add three private lateinit properties to the created test class:
- The
repositoryproperty contains theUserRepositorymock. - The
emailServiceproperty contains theEmailServicemock. - The
serviceproperty contains the system under test (aUserAccountRegistrationServiceobject).
After we have added these properties to our test class, its source code looks as follows:
class UserAccountRegistrationServiceExtensionTest {
private lateinit var repository: UserRepository
private lateinit var emailService: EmailService
private lateinit var service: UserAccountRegistrationService
}
2. Annotate the repository and emailService properties with the @MockK annotation. This annotation identifies the properties that contain mocks and instructs the MockK JUnit Jupiter extension to create the required mock objects. After we have annotated these properties with the @MockK annotation, the source code of our test class looks as follows:
import io.mockk.impl.annotations.MockK
class UserAccountRegistrationServiceExtensionTest {
@MockK
private lateinit var repository: UserRepository
@MockK
private lateinit var emailService: EmailService
private lateinit var service: UserAccountRegistrationService
}
3. Annotate the service property with the @InjectMockKs annotation. This annotation identifies the property initialized by the MockK JUnit Jupiter extension and instructs it to replace the dependencies of the created object with mocks. After we have done this, the source code of our test class looks as follows:
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
class UserAccountRegistrationServiceExtensionTest {
@MockK
private lateinit var repository: UserRepository
@MockK
private lateinit var emailService: EmailService
@InjectMockKs
private lateinit var service: UserAccountRegistrationService
}
4. Register the used JUnit Jupiter extension by annotating our test class with the @ExtendWith annotation. After we have registered the MockKExtension, the source code of our test class looks as follows:
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(MockKExtension::class)
class UserAccountRegistrationServiceExtensionTest {
@MockK
private lateinit var repository: UserRepository
@MockK
private lateinit var emailService: EmailService
@InjectMockKs
private lateinit var service: UserAccountRegistrationService
}
We have now configured the system under test by using the MockK JUnit Jupiter extension. Let's take a look at the pros and cons of this approach:
Pros:
- Tests are isolated. The MockK JUnit Jupiter extension ensures that every test function gets clean mocks. In other words, it makes sure that stubbed functions and verifications cannot leak from one test to another.
- Minimal Boilerplate. We don't have to manually invoke the
mockk()function or invoke the constructor of the tested class. We can simply describe what we want by using the@MockKand@InjectMockKsannotations, and the MockK JUnit Jupiter extension will do the hard lifting for us. - Clean Class Structure. Because the MockK JUnit Jupiter extension creates the mock objects and the tested object, our test class looks clean. All properties are listed at the top of the test class, and the
@MockKand@InjectMockKsannotations make it clear what's mocked and what's tested.
Cons:
- Requires nullable or lateinit properties. Because the test class properties are initialized by the MockK JUnit Jupiter extension after the test class is instantiated (and not in a test class constructor), we are forced to use nullable types or lateinit properties. This is a problem because:
- If we use a lateinit property: we must use
varinstead ofvaland we cannot rely on compile-time null safety. In other words, the property can be accidentally reassigned, or we might simply forget to assign its value. The problem is that instead of a compiler error, we get a runtime error during test execution. - If we use a nullable type: we have to pay the "null-safety" tax (either use the "bang-bang" operator (
!!) or do safe calls (?.)), we have to provide a default value (null) that's immediately replaced either with a mock or the tested object, and we mask the intent of the property because in Kotlin a nullable type typically means that the value can be missing which isn't the case here.
- If we use a lateinit property: we must use
- Hides bad design. Because it's so easy to create a new mock and inject it into the system under test, we might not realize that the system under test has too many dependencies until it's too late. In other words, we don't feel the "constructor friction" that acts as a design tool.
- Relies on reflection and "magic". The MockK JUnit Jupiter extension relies heavily on reflection. This leads into a "magic" behavior that's harder to debug than plain Kotlin code. Also, if something goes wrong, it's likely that we will get a confusing error message at runtime instead of a clean compilation error.
We can now get the required dependencies with Maven and Gradle, and we understand how we can create mocks with MockK. Let's summarize what we learned from this blog post.
Summary
This blog post has taught us four things:
- If we want to use the standard MockK API with Gradle, we have to add the
mockkdependency to thetestImplementationdependency configuration. We don't have to specify the variant we want to use because Gradle can read its own .module metadata and resolve the correct variant automatically. - If we want to use the standard MockK API with Maven, we have to add the
mockk-jvmdependency to thetestscope. We must explicitly specify the variant we want to use because Maven cannot read Gradle's .module metadata. - We must choose whether we want to couple our tests to a specific test instance lifecycle mode or ensure that our tests are working as expected regardless of the lifecycle configuration. This decision dictates how we can configure the system under test.
- We must make a trade-off between automation and design feedback. The
MockKExtensionand annotations reduce boilerplate code, but the manual configuration acts as an architectural design tool which alerts us when a class has too many dependencies.
P.S. You can get the example application from Github.