Unit testing kotlin coroutines on Android with JUnit 5
JUnit 5 extensions vs JUnit 4 rules
If you unit test coroutines in your Android app, you have already seen this exception:
Exception in thread "main @coroutine#1" 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 those of you who use or plan to use JUnit 5 in your projects, I’ll show how to write a JUnit 5 test extension that fixes this problem.
JUnit 4
We know what caused the exception above: Dispatchers.Main
is not available in unit tests because it relies on Looper.getMainLooper()
from Android SDK. As the hint in the message says, you can fix it by mocking Dispatchers.Main
, and there are articles out there explaining how to do it with JUnit 4. For example, here is a solution for JUnit 4 that invokes Dispatchers.setMain()
and Dispatchers.resetMain()
before/after each test:
JUnit 5
If you are using JUnit 5 / Jupiter in your project, the solution above won’t work. A concept of rules was deprecated in JUnit 5 in favour of a new concept of test extensions. Let’s convert the rule above to a JUnit 5 extension:
JUnit 5 beforeEach(ExtensionContext)
and afterEach(ExtensionContext)
are equivalent to JUnit 4 starting(Description)
and finished(Description)
: they are called before and after each test method annotated with @Test
. The extension above will mock Dispatchers.Main
before each test and reset the mock afterwards.
And now let’s add this extension to a test class:
More about JUnit 5 extensions here: https://junit.org/junit5/docs/current/user-guide/#extensions
Bonus: Power of time machine
Notice that CoroutinesTestExtension
from above implements TestCoroutineScope
interface (by delegating it to a TestCoroutineScopeImpl
that delegates it to the same TestCoroutineDispatcher
instance that replaces Dispatchers.Main
). It means that you can control the execution of coroutines on Dispatchers.Main
by calling
coroutinesTestExtension.pauseDispatcher()
coroutinesTestExtension.resumeDispatcher()
coroutinesTestExtension.advanceTimeBy(delayTimeMillis)
coroutinesTestExtension.advanceUntilIdle()
Rx fans will notice similarities to the API of RxTestScheduler
.
Note that this does not affect coroutines running on other dispatchers, e.g. Dispatchers.IO
. If your class under test uses other dispatchers, consider passing them as a constructor argument and mocking them with a TestCoroutineDispatcher
in unit tests.
P.S.
If you want to migrate your unit tests from JUnit 4 to JUnit 5, replace the JUnit 4 dependency with
testImplementation "org.junit.jupiter:junit-jupiter:$junit_jupiter_version"
and replace your JUnit 4 imports with JUnit 5, e.g.:
// replace
import org.junit.Test
// with
import org.junit.jupiter.api.Test
Happy coding!