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
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
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.
Running the first test
Open the MainViewModelTest.kt file in the app/test directory. This file already contains the following test.
Run the test by pressing the icon in the gutter right next to the test function name and selecting Run MainViewModelTest.Gi..
The test should run without any errors and you should see a green checkmark saying your test passed.
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.
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.
Go to the MainViewModelTest file and add a new test that tests launchASuspendFunction.
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.
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.
Create a function that will run after every test to clean up.
Now if you run the test, because we replaced the main dispatcher, it passes as expected!
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.
Run the test again and you will see that it still passes.
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.
And also update the test to expect 2 calls to repositorySuspendingFunction
Now if you run the test, it fails! We expected the function to be called twice, but it was only called once.
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.
Create a class CoroutineDispatcherProvider
Add CoroutineDispatcherProvider as a dependency to MainViewModel
In app/di/viewModelModule update the view model constructor
Update the setUp function of the test class
Finally update the function we are testing to use the dispatcher provider
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.
We can apply this rule as
We can also create a BaseTest class that will apply the rule. Create a new file BaseTest
And make the MainViewModelTest class extend from BaseTest
We can now get rid of the dispatchers related code in the setup function of MainViewModelTest
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.