Testing Coroutines — Simplified
I assume you know how coroutines work and how to get the most out of them.
Now let me help you build test for it.
Take a look at this function in some RegisterViewModel Viewmodel
class RegisterViewModel: ViewModel(){
private val _isLoading: MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
fun register() {
viewModelScope.launch {
_isLoading.emit(true)
delay(1000)
_isLoading.emit(false)
}
}
}
Let’s try writing test to test this specific function
class Test {
private val viewModel: RegisterViewModel
@Before
fun setup(){
viewModel = RegisterViewModel()
}
@Test
fun testRegistration() = runTest {
assertk.assertThat(viewModel.isLoading.value).isFalse()
viewModel.register()
assertk.assertThat(viewModel.isLoading.value).isTrue()
assertk.assertThat(viewModel.isLoading.value).isFalse()
}
}
We are assuming the initial value will be false,
then we call the function, and expect the value to be update to true
and lastly, again back to false.
But if you run this, you will get the following error:
🛑 Module with the Main dispatcher had failed to initialise. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
Because, by default viewmodelScope
uses Main dispatcher, which is part of Android, we can not access it in Unit Test.
The fix?
We can tell the test class to use a specific Dispatcher through out the test.
In setup
function, which is annotated with @Before
we can use the following line of code:Dispatchers.setMain(StandardTestDispatcher())
And reset it in tearDown
function annotated with @After
Dispatchers.resetMain()
The StandardTestDispatcher which is designed for testing, will be used through out the test function. Explained in details below.
Ok, now we set the dispatcher, and the test is running successfully but it fails, every single time. Why?
Let me explain,
The register function, launches the coroutine and finishes immediately, we are not waiting for the coroutine to finish, we are asserting it right away. So by the time we asserting it, the coroutine may not even have started.
We need more control on underlying coroutine.
That’s where StandardTestDispatcher
comes to rescue.
It provides you tools to command the coroutine.
We just need to pass an instance of StandardTestDispatcher
to runTest
function, so we can control it as we want.
StandardTestDispatcher
will NOT enter the launch block automatically, in the function being tested, unless you tell it to.
For our function, it means viemodelScope.launch
will never get executed.
To execute it, we call runCurrent()
function, this will check which coroutine has been launched
but not executed, and will execute it.
If we had more than one
launch
blocks, we will have to callrunCurrent
for each of them.
Here is how our update test code looks like:
@Test
fun testRegistration() = runTest(dispatcher) {
assertk.assertThat(viewModel.isLoading.value).isFalse()
viewModel.register()
runCurrent()
assertk.assertThat(viewModel.isLoading.value).isTrue()
assertk.assertThat(viewModel.isLoading.value).isFalse()
}
We are making sure the coroutine get’s executed before checking isTrue
, by calling runCurrent
It will still fail, not like previously, but the last assertion, we are expecting false, but it will be true.
Why?
Because of delay task, even though we made sure the coroutine get’s executed before we made the isTrue
assertion, we are still not waiting for delay, and the delay is not getting skipped automatically either.
Fix?
advanceUntilIdle()
It will, as the name suggests, advance until idle, or in another words, complete the coroutine.
So if we put it before isFalse
assertion, everything will fall in place and code will execute properly.
Here is the updated and working test function:
@Test
fun testRegistration() = runTest(dispatcher) {
assertk.assertThat(viewModel.isLoading.value).isFalse()
viewModel.register()
runCurrent()
assertk.assertThat(viewModel.isLoading.value).isTrue()
advanceUntilIdle()
assertk.assertThat(viewModel.isLoading.value).isFalse()
}
Wasn’t it awesome? Yes, it was.
There are more tools in our arsenal provided by StandardTestDispatcher
One of them is advanceTimeBy()
which we can use to skip the delays manually, as if we have a delay of 3000, we can use advanceTimeBy(3005)
to skip that delay.
I hope it helped you.
Read more here:
Cheers,