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:
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 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:
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:
Enough talk, let us get our hands dirty!
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:
: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
:mockito-kotlin
is a library that provides helpful functions for working with Mockito in Kotlin projectsimport 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.
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.
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.
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
.
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.
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:
coVerify
{ }
, for suspend functionsverify
{ }
, for regular functionsMockito uses verify()
for verification and still requires using runTest
to support suspend functions. This still works, but is less clear than Mockk’s approach.
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.
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.