While running unit test for kotlin suspend method which uses withContext(Dispatchers.Main)
the test method fails with below exception:
My coroutine lib versions are kotlinx-coroutines-core:1.1.1 and kotlinx-coroutines-android:1.1.1
Example:
suspend fun methodToTest() {
withContext(Dispatchers.Main) {
doSomethingOnMainThread()
val data = withContext(Dispatchers.IO) {
doSomethingOnIOThread()
}
}
}
Also, when I remove the withContext(Dispatchers.Main)
it works fine.
java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.missing(MainDispatchers.kt:79)
at kotlinx.coroutines.internal.MissingMainCoroutineDispatcher.isDispatchNeeded(MainDispatchers.kt:54)
at kotlinx.coroutines.DispatchedKt.resumeCancellable(Dispatched.kt:373)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:25)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:152)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages. On Android, coroutines help to manage long-running tasks that might otherwise block the main thread and cause your app to become unresponsive.
Dispatchers. Main - Use this dispatcher to run a coroutine on the main Android thread. This should be used only for interacting with the UI and performing quick work. Examples include calling suspend functions, running Android UI framework operations, and updating LiveData objects.
The simplest way to create a coroutine is by calling the launch builder on a specified scope. It Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. The coroutine is canceled when the resulting job is canceled. The launch doesn't return any result.
All suspend functions from kotlinx. coroutines are cancellable: withContext , delay etc. So if you're using any of them you don't need to check for cancellation and stop execution or throw a CancellationException .
When running tests e.g for ViewModel that launch coroutines you are most likely to fall into the following exception
java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests, Dispatchers.setMain from kotlinx-coroutines-test module can be used
The reason behind this is the lack of Looper.getMainLooper()
on the testing environment which is present on a real application. To fix this you need to swap the Main dispatcher with TestCoroutineDispatcher
Make sure you have a coroutine-test dependency on your Gradle file
"org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_version"
SOLUTION 1 - Not scalable
Define the following on your test class -> Annotate your class with @ExperimentalCoroutinesApi
val dispatcher = TestCoroutineDispatcher()
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
Note: You can also pass Dispatchers.Main
as constructor dependency for your repositories as CoroutineDispatcher
in case you have one. It is recommended not to hardcode your dispatchers on repositories/viewmodels etc WATCH-THIS PLEASEEEEEEEE
Why not scalable: You will need to copy and paste the same code on each test class
SOLUTION 2 - Scalable [Use This - It is used by Google]
In this solution, you create the custom rule. Add a utility class on your test package
@ExperimentalCoroutinesApi
class MainCoroutineRule(
private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
If you want explanations on the utility class above, refer to this CODE-LAB
On your test class just add this the following lines and you will be good to go:
@get:Rule
val coroutineRule = MainCoroutineRule()
I think you can see why this is scalable if you have a lot of test classes.
SOLUTION 3 [I hope you don't reach here]
You can also use Dispatchers.Unconfined
LINK
A coroutine dispatcher that is not confined to any specific thread. It executes the initial continuation of a coroutine in the current call-frame and lets the coroutine resume in whatever thread that is used by the corresponding suspending function, without mandating any specific threading policy. Nested coroutines launched in this dispatcher form an event-loop to avoid stack overflows.
You can add it as follows
@Before
fun setup() {
Dispatchers.setMain(Dispatchers.Unconfined)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
Happy coding . . . .
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With