One Page

Simplifying Testing Coroutines with Test Rules

June 26, 2019

I’ve recently had to test coroutines and while constructing them I’ve had to replace the Main Dispatcher or the coroutine block would never run.

I ran into a few issues while doing this and thought it would be helpful to go through my process of fixing them all.

If you’re just looking for the final code that works and to get started, here’s all you need for that.

Issue #1: Replacing the Main Dispatcher

Moderately Good Way

Thanks to Sean McQuillan’s excellent tweet explaining different ways to make the code more testable, I found one straightforward way. Dependency injection.

One nice way to do this that doesn’t affect the production API a lot is to write it this way:

class ModelStore(
    private val dispatcher : CoroutineDispatcher = Dispatchers.Main
    private val repo: ShareVideoRepository,
    private val state: (VideoShareState) -> Unit,
) {

    private val uiScope = CoroutineScope(SupervisorJob() + dispatcher)

By using kotlin’s default parameters, we can not bother about the production case and just pass in an appropriate dispatcher to test with.

I wondered, could we go farther? Sean and the kotlin-coroutine-test repo both mention alternative ways to test this and I wanted the lowest touch, no hassle, way of testing it. Did you need to use Dependency Injection?

A better way

Turns out, the Dispatchers are service locators (almost like Dependency Injection) themselves! There was a planned api that just required you to call Dispatchers.setMain(TestCoroutineDispatcher()) and it would return that test dispatcher instead of main whenever one was requested.

You’d also have to remember to call Dispatchers.resetMain() when you’re done to avoid carrying forward the effects into unexpected areas.

So is this is as simple as putting the two calls in the test annotated methods?

@Before
fun setUp() = Dispatchers.setMain(TestCoroutineDispatcher())

@After
fun cleanUp() = Dispatchers.resetMain()

Sure! That works too! And it’s better than remembering to call them inside your coroutine testing methods.

Can we do better though?

The Best Way

I happened to be watching Chiu-Ki Chan’s mockwebserver video and noticed then that she ran into a similar situation.

She needed to start the server before the test and stop it after. For every single test. She extracted the code for that out into a Junit Test Rule.

Test Rules are a lot simpler than the code for the ActivityTestRule which you might have seen, is.

Fundamentally they let you a lot but one of the things is that you can specify some code to run before and after each test!

Here’s what a custom test rule looks like.

@ExperimentalCoroutinesApi
class CoroutineTestRule : TestRule {
    override fun apply(base: Statement?, description: Description?): Statement =
        object : Statement() {
            override fun evaluate() {
                Dispatchers.setMain(TestCoroutineDispatcher())
                base?.evaluate() // Any test statement
                Dispatchers.resetMain()
            }
        }
}

And that’s it! Once you have this test rule, you won’t need to remember to set and reset the Main Dispatcher for each of your tests.

I still ran into one last hurdle though.

Test Rules Annotations

If you’ve unit tested a bit, or sun any instrumentation test at all you would’ve seen the ubiquitous java code:

@Rule
public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule(MainActivity.class);

When writing Kotlin the annotation needs to be slightly different

@get:Rule
var activityRule = ActivityTestRule(MainActivity::class.java, false, false)

By now, we know what these Test Rules are for. But you’ll notice in Kotlin we needed to add get: to the Rule annotation.

Annotations are a way of attaching metadata to code, and in the case of the variable activityRule, we need to specify that the annotation should be generated for the getter of activityRule. Without that you’d get errors like: org.junit.internal.runners.rules.ValidationError: The @Rule 'activityRule' must be public. See the kotlin docs for more info.

With this one last change, we’re ready to rock! All future tests involving coroutines need just have this one line added to the test file and they’ll work :)

    @ExperimentalCoroutinesApi
    @get:Rule
    val coroutineTestRule = CoroutineTestRule()

To see the code for all the elements, including the imports, see here.

I hope you enjoyed this journey through all the bugs and issues we waded through to get here :) There are even a few I ran into like the duplicate TimedRunnable bug that prevents you from writing tests targeted to the coroutine library version 1.2.1, which is current as of this writing. You’ll see in the build.gradle of the gist which one you could use instead (the latest develop branch version).

Have suggestions for a topic I should cover? Send me a dm at @AniketSMK or email me at hello@[firstname][lastname].com


Aniket Kadam, author

Written by Aniket Kadam - building useful things. Follow me on Twitter