Emmanuel Enya I am a computer engineering graduate with five years of professional experience building modern Android applications. I am a huge fan of clean code because clarity is King 😄

Unit testing in Kotlin projects with Mockk vs. Mockito

5 min read 1585

Unit testing in Kotlin projects with Mockk and Mockito

Introduction

Unit testing is an age-old integral part of the software engineering practice. Most successful software products use properly written unit tests; fundamentally, unit testing verifies the correctness of a piece of code.

Writing code to test code may seem counterintuitive, but it is an art unto itself that gives a developer confidence in their code.

Below are the significant benefits we derive from writing unit tests:

  1. Catch bugs early in production code
  2. Reduce code complexity by having a unit of code doing a specific thing
  3. Serve as a good source of documentation for a developer to get insight on a project
  4. Save time (and money)
  5. Encourage writing clean and maintainable code with less coupling

Unit testing in Kotlin projects

The Kotlin programming language is fundamentally executed in the JVM environment. Its concise and fancy language features have made it popular within the community, and it is being used frequently in projects such as Android applications, Kotlin Multiplatform Mobile (KMM) apps, and spring boot applications.

There are currently two popular frameworks built to aid in effective unit testing: Mockito and Mockk. In this post, we’ll talk about each of them through the following sections:

Mockk vs. Mockito

Mockk and Mockito are libraries that help write unit tests that target JVM platforms. Mockito has been around since the early days of Android development and eventually became the de-facto mocking library for writing unit tests.

Mockito and Mockk are written in Java and Kotlin, respectively, and since Kotlin and Java are interoperable, they can exist within the same project. Essentially, both libraries can be used interchangeably in these projects, but the preeminence of Kotlin has tipped the balance in favor of Mockk for most Kotlin projects.

The following points summarize why Mockk is favored over Mockito for Kotlin projects:

  • First class support for Kotlin features
  • A pure Kotlin-mocking DSL for writing clean and idiomatic Kotlin code
  • Mocking support for final classes and methods
  • Coroutine support by default

How to write unit tests for Kotlin projects

For the purpose of this article, we will implement a simple user repository class to demonstrate writing unit tests in a Kotlin project. There are two things to note before we proceed:

  1. The development environment will be in Android Studio
  2. We’ll compare the syntax difference between both mocking libraries for the different test cases covered

Enough talk, let us get our hands dirty!



Create the user repository

First, define the interface for the repository like so:

interface UserRepository {

    suspend fun saveUser(user: User)

    suspend fun getUser(id: String): User

    suspend fun deleteUser(id: String)
}

This is basically a contract that will be implemented by the concrete class. See the code block below.

class UserRepositoryImpl constructor(
    private val dataSource: DataSource
) : UserRepository {
    override suspend fun saveUser(user: User) {
        dataSource.save(user)
    }

    override suspend fun getUser(id: String): User {
        return dataSource.get(id) ?: throw IllegalArgumentException("User with id $id not found")
    }

    override suspend fun deleteUser(id: String) {
        dataSource.clear(id)
    }
}

UserRepositoryImpl has a dependency on DataSource, through which it fulfills the contract by UserRepository.

DataSource is a simple Kotlin class. Its purpose is to store user data in memory, where it can later be retrieved. See the code block below for details:

class DataSource {

    private val db = mutableMapOf<String, User>()

    fun save(user: User) = db.let { it[user.email] = user }

    fun get(key: String): User? = db[key]

    fun clear(key: String) = db.remove(key)

    fun clearAll() = db.clear()
}

To keep things simple, I have used a mutableMap object to save a User to memory. Maps are collections that holds pairs of objects (keys and values), so it makes sense to have the user email serve as the unique key for saving and retrieving the User.

<h3=”add-library-dependencies-gradle”>Add library dependencies to Gradle

Add the following dependencies to your app-level Gradle file, like so:

//Mockk
testImplementation "io.mockk:mockk:1.12.4"

//Mockito
testImplementation "org.mockito:mockito-core:4.0.0"
testImplementation "org.mockito:mockito-inline:4.0.0"
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"

Using Mockito in a Kotlin project requires some extra dependencies for the following reasons:


More great articles from LogRocket:


  1. Kotlin classes are final by default and cannot be mocked by Mockito, hence the need for :mockito-inline:. You might think an alternative would be to add the open modifier to the class involved, but this is not recommended because it will mess up your code base and force you to define your classes as open
  2. :mockito-kotlin is a library that provides helpful functions for working with Mockito in Kotlin projects

Writing tests for system under test (SUT) using Mockk

import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.slot
import java.util.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class UserRepositoryImplTest {

    private val dataSource = mockk<DataSource>(relaxed = true)
    private val sut = UserRepositoryImpl(dataSource)

    @Test
    fun `verify correct user params are used`() = runTest {
        val user = buildUser()

        sut.saveUser(user)

        val captor = slot<User>()

        coVerify { dataSource.save(capture(captor))}

        Assert.assertEquals(user.email, captor.captured.email)
    }

    @Test
    fun `verify correct user is retrieved`() = runTest {
        val email = "[email protected]"

        coEvery { dataSource.get(any()) } returns buildUser()

        val user = sut.getUser(email)

        Assert.assertEquals(email, user.email)
    }

    @Test
    fun `verify user is deleted`() = runTest {
        val email = "[email protected]"
        sut.deleteUser(email)

        coVerify { dataSource.clear(any()) }
    }

    companion object {
        fun buildUser() = User(
            id = UUID.randomUUID().toString(),
            email = "[email protected]",
            fullName = "Emmanuel Enya",
            verificationStatus = User.VerificationStatus.Verified,
            memberShipStatus = User.MemberShipStatus.Free
        )
    }
}

The above code block is a fairly simple test class with minimal test cases. At the top level of the class body, I have mocked the data source and created an instance of the system under test.

Notice that DataSource is mocked with relax set to true, like so:

mockk<DataSource>(relaxed = true)

This kind of mock returns a simple value for all functions, allowing you to skip specifying behavior for each case. Check the Mockk documentation for more details on relaxed mocks.

Other sections of the code block will be examined side-by-side with the Mockito variant of the test class.

Writing tests for system under test (SUT) using Mockito

import java.util.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Test
import org.mockito.Mockito.*
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor

@OptIn(ExperimentalCoroutinesApi::class)
class UserRepositoryImplTestMockito {

    private val dataSource = mock(DataSource::class.java)
    private val sut = UserRepositoryImpl(dataSource)

    @Test
    fun `verify correct user params are used`() = runTest {
        val user = buildUser()

        sut.saveUser(user)

        val captor = argumentCaptor<User>()

        verify(dataSource).save(captor.capture())

        Assert.assertEquals(user.email, captor.firstValue.email)
    }

    @Test
    fun `verify correct user is retrieved`() = runTest {
        val email = "[email protected]"

        `when`(dataSource.get(any())).then { buildUser() }

        val user = sut.getUser(email)

        Assert.assertEquals(email, user.email)
    }

    @Test
    fun `verify user is deleted`() = runTest {
        val email = "[email protected]"
        sut.deleteUser(email)

        verify(dataSource).clear(any())
    }


    companion object {

        fun buildUser() = User(
            id = UUID.randomUUID().toString(),
            email = "[email protected]",
            fullName = "Emmanuel Enya",
            verificationStatus = User.VerificationStatus.Verified,
            memberShipStatus = User.MemberShipStatus.Free
        )
    }
}

The above code block is a test class written with Mockito. At the top level of the class body, we have the mocked dependency and the SUT set up in a similar fashion to how we did with Mockk.

However, one salient point to note is that there is no relaxed mock argument. The reason for this is because Mockito provides default answers for behaviors when they are not stubbed.

Conclusions

I feel Mockk shines here because mocks are not relaxed by default, which encourages you to have total control over the functions call being made by the SUT.

Argument capturing in Mockk and Mockito

argumentCaptor is a function from the mockito-kotlin extension library. It helps to capture a single argument from the mocked object, usually done in the verification block.

The Mockk variant is a slot.

Stubbing

Usually, when writing unit tests, we specify answers to function calls on mocked objects. This is referred to as stubbing in unit testing.

Using Mockito, it is declared like so:

`when`(dataSource.get(any())).then { buildUser() }

Mockito does not have inbuilt support for Kotlin coroutines, which means testing coroutines will require the use of runTest for the code to compile.

In Mockk, there is coEvery { } for stubbing coroutines and every { } for regular functions. This distinction adds clarity to the code being tested.

Verification

An important part of unit testing is to verify the method interactions of mocked objects in production code.
suspend functions can only be invoked by other suspend functions. Having coVerify{ } in place gives developers the support to stub suspend functions, which ordinarily wouldn’t be possible with the verify{ } block.

Mockk provides two functions to test suspend functions and regular functions:

  1. coVerify { }, for suspend functions
  2. verify { }, for regular functions

Mockito uses verify() for verification and still requires using runTest to support suspend functions. This still works, but is less clear than Mockk’s approach.

Conclusion

We’ve explored unit testing in Kotlin projects and how to effectively write tests with Mockk and Mockito.

Feel free to use whichever library you find attractive, but I would recommend Mockk because of how flexible it is when working with the Kotlin language.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

.
Emmanuel Enya I am a computer engineering graduate with five years of professional experience building modern Android applications. I am a huge fan of clean code because clarity is King 😄

Leave a Reply