Test Coroutine Scheduler
June 23, 2022
Quick post: If you are here, you probably ran into the following exception while writing unit tests that involve coroutines:
Exception in thread "Test worker" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
For instance, you will get this Exception if you are writing tests for Android ViewModel
that launches a coroutine via viewModelScope.launch {}
. This is because this scope is bound to Dispatchers.Main.immediate
and there is no Main Dispatcher available to your tests which run on the JVM.
Solution
The error message above actually tells us what to do (use Dispatchers.setMain
from from kotlinx-coroutines-test library).
You can create a MainDispatcherRule
which lets you set a Main Dispatcher on the TestDispatcher
First, add latest kotlinx-coroutines-test to your project test dependencies.
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
Create a MainDispatcherRule
by extending TestWatcher
:
package com.example.uistateplayground.util import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.* import org.junit.rules.TestRule import org.junit.rules.TestWatcher import org.junit.runner.Description /** * A JUnit [TestRule] that sets the Main dispatcher to [testDispatcher] * for the duration of the test. */ class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : TestWatcher() { override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { super.finished(description) Dispatchers.resetMain() } }
And finally use the new MainDispatcherRule
in your test classes that require Main Dispatcher:
import org.junit.Rule class MyViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() // the rest of the test code }
TestDispatcher implementations
There are two TestDispatcher
implementations available:
StandardTestDispatcher
UnconfinedTestDispatcher
Note that we used UnconfinedTestDispatcher
for our Main Dispatcher. The reason for that viewModelScope
uses Dispatchers.Main.immediate
and UnconfinedTestDispatcher
gives us a matching eager behavior so we get the same behavior in our tests and in production.
Now every runTest {}
will use a TestScope
with UnconfinedTestDispatcher
instead of the default StandardTestDispatcher
.
Here are the differences between StandardTestDispatcher
vs UnconfindedTestDispatcher
summarized:
StandardTestDispatcher
:
Queues up coroutines on the scheduler
You need to manually advance those coroutines
Created by default when
runTest
is used
UnconfinedTestDispatcher
:
Starts new coroutines eagerly (like the deprecated
runBlockingTest
)Use it selectively for these use cases:
when migrating tests from old APIs
As the Main dispatcher
For coroutines that collect values
What about TestCoroutineDispatcher?
You may have read about developers solving the issue above by using TestCoroutineDispatcher
.
Don’t do it. That class is deprecated in 1.6 with a message suggesting the solution described above.
@Deprecated("The execution order of `TestCoroutineDispatcher` can be confusing, and the mechanism of pausing is typically misunderstood. Please use `StandardTestDispatcher` or `UnconfinedTestDispatcher` instead.") public class TestCoroutineDispatcher