Say goodbye to Flakiness : Testing Coroutines and Kotlin Flows [Part 1]

Shounak Mulay
February 4, 2022
Contents

As an android developer, you couldn’t have overlooked the flaky nature of testing coroutines in Kotlin. But there is a way out, all you need to do is make sure the tests are setup correctly. This tutorial will show you exactly that step by step.

Note: In this tutorial we’re using an Android project.
However, this isn’t limited to Android and can be used for any Kotlin project


Before diving in, you can familiarize yourself with unit testing with JUnit or Kotlin Coroutines.

Starter Project

To begin, clone the start branch of this git repository.

Next, open the project in Android Studio. The starter project is based on the MVVM architecture but without the service layer. It already contains a repository, a view model, and a test class for the view model.

Here is how your project should look

Directory structure showing the different folders and files of the started project.

Here, you will be testing the view model. The repository is just a placeholder dependency of the view model. You will mock this repository in your tests.

Before you start writing the test, build and run the project in Android studio.

The app should run without any errors and you’ll see a “Hello World” on your phone.

A preview of the running Android application in the simulator.
Hello World!

Running the first test

Open the MainViewModelTest.kt file in the app/test directory. This file already contains the following test.


@Test
fun `Given suspendingFunction is called, When no error occurs, 
Then repositorySuspendingFunction should be
invoked successfully`() = runBlocking {
    mainViewModel.suspendingFunction()

    verify(mainRepository, times(1)).repositorySuspendingFunction()
}


Run the test by pressing the icon in the gutter right next to the test function name and selecting Run MainViewModelTest.Gi..

Run the MainViewModelTest by selecting the play icon in the gutter.

The test should run without any errors and you should see a green checkmark saying your test passed.

Success results of running the MainViewModelTest

Let’s understand what this test tests.

The MainViewModel.kt file has a suspending function called suspendingFunction  which in turn calls another suspending function in the repository.


suspend fun suspendingFunction() {
    mainRepository.repositorySuspendingFunction()
}

This test uses runBlocking. It allows you to call suspend functions within its body by blocking a new coroutine. It blocks the current thread until it completes. This is the behaviour that we want in a test, we do not want any other code running on the thread while the test is in progress.

If the function under test has any calls to delay, we can use runBlockingTest instead. It works the same as runBlocking with a few differences.

While runBlocking will wait for the amount of the delay, runBlockingTest will skip past any delay blocks present and will instantly enter the coroutine blocks. This makes the tests run fast.

Testing functions that launch a new coroutine

Now let's write a test for another function in the view model that calls the same function in the repository, but does that by launching a new coroutine, instead of being a suspend function itself.


fun launchASuspendFunction() = viewModelScope.launch {
    mainRepository.repositorySuspendingFunction()

Go to the MainViewModelTest file and add a new test that tests launchASuspendFunction.


@Test
fun `Given launchASuspendFunction is called, When no error occurs, 
Then repositorySuspendingFunction should be 
invoked successfully`() = runBlocking {
    mainViewModel.launchASuspendFunction()

    verify(mainRepository, times(1)).repositorySuspendingFunction()
}

Run this new test. You will get an error message saying module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used.

Failure results of running the MainViewModelTest test case. Here the test case tests a function that launches a coroutine.

The error means that it was not possible to launch a coroutine on the main thread.

Since launchASuspendFunction launches a new coroutine on the viewModelScope without any dispatcher provided as an argument, it uses Dispatchers.

Main by default. In the test environment, Looper.getMainLooper() is not available thus the test fails.

To solve this issue we need to use TestCoroutineDispatcher which executes tasks immediately.

Set the TestCoroutineDispatcher as the main dispatcher in the setUp function that executes before every test.


@Before
fun setUp() {
    mainRepository = mock()
    Dispatchers.setMain(TestCoroutineDispatcher())
    mainViewModel = MainViewModel(mainRepository)
}

Create a function that will run after every test to clean up.


@After
fun cleanup() {
    Dispatchers.resetMain()
}

Now if you run the test, because we replaced the main dispatcher, it passes as expected!

Success results of running the MainViewModelTest test after adding the TestCoroutineDispatcher.

Always Inject Dispatchers

Now that we have fixed the problem of Dispatchers. Main, let's try some other dispatchers.

Go to the MainViewModel and update the viewModelScope.launch method on launchASuspendFunction to accept a dispatcher. Let's use the Default dispatchers.


fun launchASuspendFunction() = viewModelScope.launch(Dispatchers.Default) {
    mainRepository.repositorySuspendingFunction()

Run the test again and you will see that it still passes.

Tests pass after adding the default dispatcher.

So everything is fine right? Not quite.

We could have functions that do more than just one operation or function call.

Let's call the repository function one more time.


fun launchASuspendFunction() = viewModelScope.launch(Dispatchers.Default) {
    mainRepository.repositorySuspendingFunction()
    mainRepository.repositorySuspendingFunction()

And also update the test to expect 2 calls to repositorySuspendingFunction


@Test
fun `Given launchASuspendFunction is called, When no error occurs, 
Then repositorySuspendingFunction should 
be invoked successfully`() = runBlocking {
    mainViewModel.launchASuspendFunction()

    verify(mainRepository, times(2)).repositorySuspendingFunction()

Now if you run the test, it fails! We expected the function to be called twice, but it was only called once.

Failure on running the MainViewModelTest.

We replaced the Main dispatcher with the TestCoroutineDispatcer. But now the function under test is launching a new coroutine on the Default dispatcher. To make the test run correctly we need to replace the Default dispatcher with TestCoroutineDispatcher as well. But there is no method like Dispatchers.setMain to replace the Default or IO dispatcher.

The solution to this is to inject dispatchers as a dependency into the classes that use them.

dispatchers

Create a class CoroutineDispatcherProvider


data class CoroutineDispatcherProvider(
  val main: CoroutineDispatcher = Dispatchers.Main,
  val default: CoroutineDispatcher = Dispatchers.Default,
  val io: CoroutineDispatcher = Dispatchers.IO

Add CoroutineDispatcherProvider as a dependency to MainViewModel


class MainViewModel(
  private val mainRepository: MainRepository,
  private val dispatcherProvider: CoroutineDispatcherProvider
): ViewModel() {

In app/di/viewModelModule update the view model constructor


val viewModelModule = module {
    viewModel { MainViewModel(get(), CoroutineDispatcherProvider()) }
}

Update the setUp function of the test class


@Before
fun setUp() {
    mainRepository = mock()
    val testDispatcher = TestCoroutineDispatcher()
    Dispatchers.setMain(TestCoroutineDispatcher())
    val coroutineDispatcherProvider = CoroutineDispatcherProvider(
        main = testDispatcher,
        default = testDispatcher,
        io = testDispatcher
    )
    mainViewModel = MainViewModel(mainRepository, coroutineDispatcherProvider)
}

Finally update the function we are testing to use the dispatcher provider


fun launchASuspendFunction() = viewModelScope.launch(dispatcherProvider.default) {
    mainRepository.repositorySuspendingFunction()
    mainRepository.repositorySuspendingFunction()
}

Now if we run the test, it will pass again no matter what dispatcher we use.

Coroutine Scope Rule

Manually creating a test dispatcher and CoroutineDispatcherProvider for every test class is a bit tedious. We can extract this work into a JUnit rule.

Create a class CoroutineScopeRule in the test directory.


@ExperimentalCoroutinesApi
class CoroutineScopeRule(
    private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(),
    var dispatcherProvider: CoroutineDispatcherProvider = CoroutineDispatcherProvider()
): TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) {

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(dispatcher)
        dispatcherProvider = CoroutineDispatcherProvider(
            main = dispatcher,
            default = dispatcher,
            io = dispatcher
        )
    }

    override fun finished(description: Description?) {
        super.finished(description)
        cleanupTestCoroutines()
        Dispatchers.resetMain()
    }
    
}

We can apply this rule as


@get:Rule
val coroutineScope = CoroutineScopeRule()

We can also create a BaseTest class that will apply the rule. Create a new file BaseTest


open class BaseTest {
    
    @get:Rule
    val coroutineScope = CoroutineScopeRule()

And make the MainViewModelTest class extend from BaseTest


class MainViewModelTest: BaseTest()

We can now get rid of the dispatchers related code in the setup function of MainViewModelTest


@Before
fun setUp() {
    mainRepository = mock()
    mainViewModel = MainViewModel(mainRepository, coroutineScope.dispatcherProvider)

The cleanup function can be deleted as that part is already handled by the rule applied in the BaseTest class.

Where to go from here

You now have a setup that can help you write unit tests that deal with coroutines and any dispatchers! Bravo!

You can look at the complete code after part 1 at https://github.com/shounakmulay/KotlinFlowTest/tree/part1-end

In Par, for more!

I hope you enjoyed this tutorial on dealing with coroutines. Join us on Twitter to leave comments about this, but it will be good to go if you change the structure tutorial and your findings as you work with them!

About the Author

A master of Android and Flutter apps, Shounak is busy leading teams of mobile developers at Wednesday Solutions by day. A gaming enthusiast, you can find him in front of a screen looking for a good movie to watch. An avid fan of photography, Shounak tries to capture different landscapes at sunset.

Speak with an expert

Looking for help with development, design, or product strategy? We're here to help.

Schedule a meeting
Get in touch

More Tutorials

Schedule a meeting with Wednesday. Tell us about your product development journey and let's figure out a way to work together.
Talk to us
The Wednesday Show - A show about the design, development, and business of digital products. Every week the founders of Wednesday talk about their learnings of building digital products.
Psst! Listen to our podcast The Wednesday Show here
Close Icon