takharsh
takharsh

Reputation: 2688

Kotlin coroutine unit test fails with "Module with the Main dispatcher had failed to initialize"

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)

Upvotes: 68

Views: 55195

Answers (8)

Emmanuel Mtali
Emmanuel Mtali

Reputation: 4963

UPDATE 2024

Add the following dependency on your gradle. Documentation here

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"

Add the following Custom TestRule as a utility for your tests

// Reusable JUnit4 TestRule to override the Main dispatcher
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

In your test do the following

@get:Rule
val mainDispatcherRule = MainDispatcherRule()

OLD ANSWER

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 [used by Google, but deprecated. use code from top of the post ]

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()
    }

Upvotes: 121

Mario Kutlev
Mario Kutlev

Reputation: 5086

For JUnit 5 tests you can use the following solution:

Step 1: Create JUnit 5 extension

/**
 * Sets the main coroutine dispatcher to a test dispatcher for unit testing.
 * A test dispatcher provides control over the execution of coroutines.
 */
internal class MainCoroutineExtension : BeforeEachCallback, AfterEachCallback {

    override fun beforeEach(context: ExtensionContext) {
        Dispatchers.setMain(dispatcher)
    }

    override fun afterEach(context: ExtensionContext) {
        Dispatchers.resetMain()
    }

    companion object {
        val dispatcher = UnconfinedTestDispatcher()
    }
}

Step 2: Create an annotation for the extension

/**
 * Sets the main coroutine dispatcher for JUnit 5 tests.
 * You should need this annotation mainly for ViewModel tests.
 */
@Target(AnnotationTarget.CLASS)
@ExtendWith(MainCoroutineExtension::class)
annotation class MainCoroutine

Step 3: Use the extension on ViewModel test classes

@MainCoroutine
internal class HomeViewModelTest { ...

Note: Instead of creating annotation you can use directly the extension. I find the annotation solution cleaner.

@ExtendWith(MainCoroutineExtension::class)
internal class HomeViewModelTest { ...

Upvotes: 0

Chathu_sm
Chathu_sm

Reputation: 85

TestCoroutineDispatcher is now deprecated. the suggested way is to use UnconfinedTestDispatcher like below

    // Reusable JUnit4 TestRule to override the Main dispatcher
@ExperimentalCoroutinesApi
class MainDispatcherRule(
    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),
) : TestWatcher() {
    override fun starting(description: Description) {
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        Dispatchers.resetMain()
    }
}

refer Setting the Main dispatcher

Upvotes: 0

SøsKø
SøsKø

Reputation: 17

I got the same issue and I found the solution here kotlinx.coroutines the solution is to use Coroutines version 1.1.0 I try it and Now it work perfectly

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0"

Upvotes: 0

Andrew Chelix
Andrew Chelix

Reputation: 1212

This is thrown because Dispatcher.Main is missing. It is Android based and therefore cannot be used for unit tests. The solution lies in the documentation by the Coroutines team. Below is an example that solved my problem and is included in the documentation.

class SomeTest {
    
    private val mainThreadSurrogate = newSingleThreadContext("UI thread")

    @Before
    fun setUp() {
        Dispatchers.setMain(mainThreadSurrogate)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
        mainThreadSurrogate.close()
    }
    
    @Test
    fun testSomeUI() = runBlocking {
        launch(Dispatchers.Main) {  // Will be launched in the mainThreadSurrogate dispatcher
            // ...
        }
    }
}

What you should note is newSingleThreadContext("UI Thread") and Dispatchers.setMain(mainThreadSurrogate) which is called before any tests in order to create a Main Dispatcher.

Upvotes: 5

jeprubio
jeprubio

Reputation: 18002

In my case I had set the main coroutines dispatcher for unit testing and still saw some errors of that from time to time.

I've added to the build.gradle as in here:

android {
  // ...
  testOptions { 
    unitTests.returnDefaultValues = true
  }
}

and I don't see that error anymore.

Upvotes: 11

Vahab Ghadiri
Vahab Ghadiri

Reputation: 2420

now you can add this to your test :

Dispatchers.setMain(Dispatchers.Unconfined)

or other dispatcher.. it's experimental but it works!

Upvotes: 17

r2rek
r2rek

Reputation: 2233

You don't have access to Dispatchers.Main in unit testing

See https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/

Dispatchers.Main Delegation part explains in detail what you need to do.

Upvotes: 20

Related Questions