Blog Infos
Author
Published
Topics
, , , ,
Published

Hey there! In this article, we’re going to explore how to handle asynchronous code in your unit tests using MockK. We’ll cover:

  1. How to mock suspend functions
  2. How to mock Flows
  3. Using coEvery {} and coVerify {}
  4. 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Intro to unit testing coroutines with Kotest & MockK

In this workshop, you’ll learn how to test coroutines effectively using the Kotest and MockK libraries, ensuring your app handles concurrent tasks efficiently and with confidence.
Watch Video

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michalik
Kotlin GDE

Intro to unit testing coroutines with Kotest & MockK

Jaroslaw Michali ...
Kotlin GDE

Jobs

No results found.

Best Practices and Common Pitfalls
Best Practices
  1. Use runTest for coroutine-based tests to avoid flakiness.
  2. Keep mocks simple: Avoid overcomplicating your stubs with too much logic.
  3. Verify interactions: Use coVerify to ensure your suspending functions are called as expected.
  4. Mocking Flows: Use flowOf for simple scenarios and flow for more complex ones.
Common Pitfalls
  1. Not using InstantTaskExecutorRule: This can cause LiveData tests to fail due to threading issues.
  2. Forgetting coVerify: Without verification, you might miss if a suspend function was never called.
  3. Blocking the Main Thread: Ensure that you’re using runTest to handle coroutines properly.
  4. 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 {} and coVerify {}.
  • How to test Flows using flowOf and toList().
  • 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.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE
Menu