MockK over the years became my go-to library for mocking suspend functions. Its intuitive syntax and built-in coroutine support simplify testing asynchronous operations. With MockK, you can easily replicate complex behaviors of coroutine-based functions, ensuring tests are reflective of real-world scenarios.
Mock – fake version of an object used in testing to check how other parts of a program interact with it.
Stub – fake part of the program that always returns the same result.
This article will show you how to use both in testing Kotlin code with MockK.
Prerequisites
Lets suppose that in our system we have dependency on use case with suspend method:
interface ProvideCurrentUser {
suspend fun execute(): UserDetails
}
data class UserDetails(val id: String, val name: String)
This use case is responsible for resolving user currently logged in to app.
We have also dependency on repository, also with suspend function getUserById:
interface CommentsRepository {
suspend fun getByUserId(userId: String): List<Comment>
}
data class Comment(val id: String, val content: String)
Which will fetch all comments for specific user.
Our goal is to fetch currently logged in user comments:
class FetchCurrentUserComments(
private val provideUserDataUseCase: ProvideCurrentUser,
private val commentsRepository: CommentsRepository
) {
suspend fun getCurrentUserComments(): List<Comment> {
val user = provideUserDataUseCase.execute()
return commentsRepository.getByUserId(user.id)
}
}
System Under Test implementation
Test case implementation
Now let’s write a test case for that class. We will start with preparing our “given”/”arrange” section:
Normally we would write stub with MockK like this:
@Test
fun `it should fetch comments for current user`() {
val givenUser = UserDetails(
id = "fake_id_1",
name = "Jarek"
)
val provideCurrentUser: ProvideCurrentUser = mockk()
every { provideCurrentUser.execute() } returns givenUser
}
But ProvideCurrentUser.execute is suspend function, so with MockK we should use coEvery for defining stub:
@Test
fun `it should fetch comments for current user`() {
val givenUser = UserDetails(
id = "fake_id_1",
name = "Jarek"
)
val provideCurrentUser: ProvideCurrentUser = mockk()
coEvery { provideCurrentUser.execute() } returns givenUser
}
Use coEvery for stubbing suspend methods
Every provideCurrentUser.execute() call will return predefined value: givenUser.
Now we can proceed with defining mock for CommentsRepository. In our test we’d like to verify that getByUser was invoked with proper parameters, so we need to create mock for repository dependency:
@Test
fun `it should fetch comments for current user`() {
val givenUser = UserDetails(
id = "fake_id_1",
name = "Jarek"
)
val provideCurrentUser: ProvideCurrentUser = mockk()
coEvery { provideCurrentUser.execute() } returns givenUser
val commentsRepository: CommentsRepository = mockk()
coEvery { commentsRepository.getByUserId(any()) } returns emptyList()
}
In CommentsRepository, method getByUserId is also suspend function, so while stubbing we should also use coEvery.
Nevertheless, in this particular test case we don’t use any specific return value, so we can use MockK built-in mechanism for providing default values:
@Test
fun `it should fetch comments for current user`() {
val givenUser = UserDetails(
id = "fake_id_1",
name = "Jarek"
)
val provideCurrentUser: ProvideCurrentUser = mockk()
coEvery { provideCurrentUser.execute() } returns givenUser
val commentsRepository: CommentsRepository = mockk(relaxed = true)
}
Use mockk(relaxed = true) to create mock with default values
Now lets finish setting up our test doubles and create system under test.
The next step is “when”/”act” section: getCurrentUserComments in System Under Test is also suspend function – to call it in regular Junit5 test we have to provide some coroutine context.
Normally we would use runBlockingTest from kotlinx.coroutines.test package, but since we are ignoring async behavior in this test case, we can use regular runBlocking.
@Test
fun `it should fetch comments for current user`() {
val givenUser = UserDetails(
id = "fake_id_1",
name = "Jarek"
)
val provideCurrentUser: ProvideCurrentUser = mockk()
coEvery { provideCurrentUser.execute() } returns givenUser
val commentsRepository: CommentsRepository = mockk(relaxed = true)
val sut = FetchCurrentUserComments(provideCurrentUser, commentsRepository)
runBlocking {
sut.getCurrentUserComments()
}
coVerify { commentsRepository.getByUserId("fake_id_1") }
}
coVerify – to perform verifications on suspend methods
On the very end of this test case we have assertion – MockK function coVerify will verify if given function on mock was invoked with given parameters.
Summary
To add Mockk to your Gradle project, put this line in dependencies block:
testImplementation 'io.mockk:mockk:1.10.2'
Mocking and stubbing suspend function is necessary if you want to properly test code that relies on coroutines – if you don’t want to write custom fake for each test double needed – you may want to make use of MockK library for mocking coroutines calls, as well as for mocking classes and methods without suspend modifier.
Want to dive deeper?
- Writing Unit Tests for Coroutines
- Introduction to Kotest and MockK
- Testing Coroutines with Kotest and MockK
- Testing StateFlow for Android
You get… - Instant unlimited access to masterclass recording
- Instant access to repository and supporting materials
- You’ll be first to know about upcoming workshops and masterclasses