Hey there! In this article, we’re going to explore how to handle asynchronous code in your unit tests using MockK. We’ll cover:
- How to mock suspend functions
- How to mock Flows
- Using
coEvery {} and
coVerify {}
- Best practices and common pitfalls
Let’s jump right in!
. . .
Mocking Suspend Functions
Why Mock Suspend Functions?
Suspend functions are at the heart of coroutine-based asynchronous programming in Kotlin. When your code depends on suspend functions, you need to mock these dependencies to test your logic in isolation.
Basic Example of Mocking Suspend Functions
Imagine you have a UserRepository
with a suspend function:
class UserRepository { suspend fun getUser(id: Int): String { delay(1000) // Simulate a network call return "John Doe" } }
Now, let’s write a test for a ViewModel that calls this function:
class UserViewModel(private val userRepository: UserRepository) : ViewModel() { val userName = MutableLiveData<String>() fun loadUser(userId: Int) { viewModelScope.launch { val name = userRepository.getUser(userId) userName.value = name } } }
Mocking with coEvery {}
and coVerify {}
Here’s how you can mock the suspend function in your test:
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test class UserViewModelTest { @get:Rule val rule = InstantTaskExecutorRule() @Test fun `loadUser updates userName LiveData`() = runTest { // Arrange val mockRepository = mockk<UserRepository>() val viewModel = UserViewModel(mockRepository) // Stub the suspend function coEvery { mockRepository.getUser(1) } returns "John Doe" // Act viewModel.loadUser(1) // Assert coVerify { mockRepository.getUser(1) } assertEquals("John Doe", viewModel.userName.value) } }
Key Points:
coEvery {}: Used to mock suspend functions.
coVerify {}: Verifies that the suspend function was called.
runTest: Allows you to test coroutine code synchronously.
Mocking Flows
Why Mock Flows?
Flows are a powerful way to handle streams of data asynchronously. Testing code that relies on Flows ensures that your data streams behave as expected.
Example of a Repository Returning a Flow
Let’s say you have a UserRepository
that returns a Flow
of user names:
class UserRepository { fun getUserFlow(): Flow<String> = flow { emit("John Doe") delay(1000) emit("Jane Doe") } }
Mocking a Flow with flowOf
Here’s how to mock the Flow in a test:
import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlinx.coroutines.flow.toList import org.junit.Assert.assertEquals import org.junit.Test class UserRepositoryTest { @Test fun `getUserFlow emits correct values`() = runTest { // Arrange val mockRepository = mockk<UserRepository>() coEvery { mockRepository.getUserFlow() } returns flowOf("John Doe", "Jane Doe") // Act val result = mockRepository.getUserFlow().toList() // Assert assertEquals(listOf("John Doe", "Jane Doe"), result) } }
Key Points:
- Use flowOf to create simple Flow mocks.
- toList() collects all the emissions of the Flow for easy assertions.
Using coEvery {}
and coVerify {}
When to Use coEvery
and coVerify
- coEvery {} is for stubbing suspend functions and Flow-returning functions.
- coVerify {} checks if suspend functions were called during the test.
Example: Combining Suspend Functions and Flows
Imagine you have a repository with a suspend function and a Flow:
class UserRepository { suspend fun getUser(id: Int): String = "John Doe" fun getUserFlow(): Flow<String> = flowOf("John Doe", "Jane Doe") }
Here’s a combined test:
import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Test class UserRepositoryCombinedTest { @Test fun `test both suspend function and Flow`() = runTest { // Mock repository val mockRepository = mockk<UserRepository>() // Stub suspend function coEvery { mockRepository.getUser(1) } returns "John Doe" // Stub Flow coEvery { mockRepository.getUserFlow() } returns flowOf("Jane Doe") // Test suspend function val userName = mockRepository.getUser(1) assertEquals("John Doe", userName) // Test Flow val flowResult = mockRepository.getUserFlow().toList() assertEquals(listOf("Jane Doe"), flowResult) // Verify the calls coVerify { mockRepository.getUser(1) } coVerify { mockRepository.getUserFlow() } } }
Job Offers
Best Practices and Common Pitfalls
Best Practices
- Use
runTest for coroutine-based tests to avoid flakiness.
- Keep mocks simple: Avoid overcomplicating your stubs with too much logic.
- Verify interactions: Use
coVerify
to ensure your suspending functions are called as expected. - Mocking Flows: Use
flowOf
for simple scenarios andflow
for more complex ones.
Common Pitfalls
- Not using
InstantTaskExecutorRule: This can cause LiveData tests to fail due to threading issues.
- Forgetting
coVerify: Without verification, you might miss if a suspend function was never called.
- Blocking the Main Thread: Ensure that you’re using
runTest
to handle coroutines properly. - Unmocked Dependencies: Always ensure your dependencies are mocked to isolate the unit under test.
Conclusion
In this article, we covered:
- How to mock suspend functions with
coEvery {}
andcoVerify {}
. - How to test Flows using
flowOf
andtoList()
. - Best practices and common pitfalls for handling asynchronous code in tests.
Mastering these techniques will help you write reliable and maintainable tests for your coroutine-based code. Happy mocking! 🚀
This article is previously published on proandroiddev.com.