Getting Started With MockK: The Setup

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.

This blog post assumes that:

  • You know how you can create a Kotlin project with Gradle or Maven.
  • You are familiar with JUnit Jupiter.

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>
We must use different artifact IDs for Gradle and Maven because:

  • 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 RegisterUserAccountRequest class contains the information that's used to register a new user account.
  • The UserAccount class contains the information of a user account that's found from the database.
  • The UserAccountExistsException is 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 returns true if the email address given as an argument is used by an existing user account and false otherwise.
  • 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:

  1. Throw the UserAccountExistsException if the registered user account doesn't have a unique email address.
  2. Insert a new user account into the database.
  3. Send the welcome email to the user.
  4. 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 MockKExtension together with the @MockK and @InjectMockKs annotations.

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 repository property contains the UserRepository mock.
  • The emailService property contains the EmailService mock.
  • The service property contains the system under test (a UserAccountRegistrationService object).

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:

  1. Create new UserRepository and EmailService mocks by invoking the mockk() function and store the created mocks in the repository and emailService properties.
  2. Create a new UserAccountRegistrationService object and store the created object in the service property.

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 @BeforeEach function, 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 @BeforeEach function, 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 @BeforeEach function 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 var instead of val and we cannot rely on compile-time null safety. In other words, the property can be accidentally reassigned outside of the @BeforeEach function, 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.
  • 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 @BeforeEach function, 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:

  1. Add a private and immutable repository property to our test class and ensure that it contains a UserRepository mock.
  2. Add a private and immutable emailService property to our test class and ensure that it contains an EmailService mock.
  3. Add a private and immutable service property to our test class and ensure that it contains a new UserAccountRegistrationService object. Remember to pass the UserRepository and the EmailService mocks 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 repository property contains the UserRepository mock.
  • The emailService property contains the EmailService mock.
  • The service property contains the system under test (a UserAccountRegistrationService object).

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 @MockK and @InjectMockKs annotations, 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 @MockK and @InjectMockKs annotations 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 var instead of val and 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.
  • 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 mockk dependency to the testImplementation dependency 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-jvm dependency to the test scope. 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 MockKExtension and 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.

0 comments… add one

Leave a Reply