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:

  1. StandardTestDispatcher

  2. 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
Previous
Previous

Compose sample app: UI state with Flow, offline first

Next
Next

Animating visibility vs alpha in Compose