Testing coroutines in Kotlin can be tricky. Tests need to be setup correctly to avoid flakyness.
In this tutorial you will learn how to write tests for coroutines. We will use an Android project however, the concepts can be applied to any Kotlin based project.
If you are not familiar with unit testing with JUnit or Kotlin Coroutines you can read about it here & here.
Starter Project
Clone the start
branch of this git repository https://github.com/shounakmulay/KotlinFlowTest.
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.
We will be testing the view model. The repository is just a place holder dependency of the view model. We will mock this repository in our tests.
Running the first test
The starter project already contains one test. Open the MainViewModelTest.kt
file in the app/test
directory.
@Test
fun `Given suspendingFunction is called, When no error occurs, Then repositorySuspendingFunction should be invoked successfully`() = runBlocking {
mainViewModel.suspendingFunction()
verify(mainRepository, times(1)).repositorySuspendingFunction()
}
- Let's run this test. You can do that by pressing the icon in the gutter right next to the test function name.
- Here we are testing a suspending function in the view model that calls another suspend function in the repository.
suspend fun suspendingFunction() {
mainRepository.repositorySuspendingFunction()
}
- On running this test it should pass as expected.
The test uses runBlocking
. It allows us to call suspend functions within its body by creating a new coroutine and 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 and in a more predictable manner.
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 testslaunchASuspendFunction
.
@Test
fun `Given launchASuspendFunction is called, When no error occurs, Then repositorySuspendingFunction should be invoked successfully`() = runBlocking {
mainViewModel.launchASuspendFunction()
verify(mainRepository, times(1)).repositorySuspendingFunction()
}
- On running this test, it fails with the error message
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
launcASuspendFunction
launches a new coroutine on theviewModelScope
without any dispatcher provided as an argument, it usesDispatchers.Main
by default. In the test environmentLooper.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 thesetUp
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!
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.
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.
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
data class CoroutineDispatcherProvider(
val main: CoroutineDispatcher = Dispatchers.Main,
val default: CoroutineDispatcher = Dispatchers.Default,
val io: CoroutineDispatcher = Dispatchers.IO
)
- Add
CoroutineDispatcherProvider
as a dependency toMainViewModel
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 an new fileBaseTest
open class BaseTest {
@get:Rule
val coroutineScope = CoroutineScopeRule()
}
- And make the
MainViewModelTest
class extend fromBaseTest
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 theBaseTest
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!
You can look at the complete code after part 1 at https://github.com/shounakmulay/KotlinFlowTest/tree/part1-end
In Part 2 we will look at testing Kotlin Flows. Here's the link to Part 2 blog.shounakmulay.dev/part-2-testing-corout..