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:
- Catch bugs early in production code
- Reduce code complexity by having a unit of code doing a specific thing
- Serve as a good source of documentation for a developer to get insight on a project
- Save time (and money)
- 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
- How to write unit tests in Kotlin projects
- Writing tests for system under test (SUT) using Mockk
- Writing tests for system under test (SUT) using Mockito
- Argument capturing in Mockk and Mockito
- Stubbing
- Verification
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:
- The development environment will be in Android Studio
- 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:
- 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 asopen
: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 = "enyasonjnr@gmail.com" 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 = "enyasonjnr@gmail.com" sut.deleteUser(email) coVerify { dataSource.clear(any()) } } companion object { fun buildUser() = User( id = UUID.randomUUID().toString(), email = "enyasonjnr@gmail.com", 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 = "enyasonjnr@gmail.com" `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 = "enyasonjnr@gmail.com" sut.deleteUser(email) verify(dataSource).clear(any()) } companion object { fun buildUser() = User( id = UUID.randomUUID().toString(), email = "enyasonjnr@gmail.com", 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:
coVerify
{ }
, for suspend functionsverify
{ }
, 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.
The post Unit testing in Kotlin projects with Mockk vs. Mockito appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/k2yF6ED
via Read more