Unit testing coroutines

Say I’ve got some code which looks like this:

class SomeClass() {
    fun doSomething() {
        launch(UI) {
            val data = getDataFromNet()
            updateUI(data)
        }
    }

    private suspend fun getDataFromNet() = withContext(CommonPool) {
        ...
    }
}

I’ve been thinking about the best way to unit test this. The first problem is that (especially in Android) the UI dispatcher isn’t available while testing. That’s fixed by injecting the dispatcher in the class’ constructor, and using some other dispatcher (like CommonPool) in the test. But then we get to the second, bigger problem of making sure all the code is run before the unit test finishes.

I could change doSomething() to return the Job that launch returns, and have the unit test join it. The con of that is I’m changing my API just to support testing, and exposing implementation details.

Another possibility is taking advantage of the fact that a parent coroutine won’t exit until its child coroutines are finished. I could use runBlocking in the unit test and pass its coroutine context to the class, which would pass it to the coroutines as the context:

@Test
fun testSomething() {
    runBlocking {
        val testObj = SomeClass(coroutineContext)
        testObj.doSomething()
        // validate
    }
}

One problem with this is that you need to create the class under test inside runBlocking so it can access the coroutine context, which isn’t the standard way. Also this completely breaks if the class explicitly sets the coroutine’s parent.

This StackOverflow question has an interesting solution. You create an interface which calls the coroutine functions, and have separate implementations for real and test. The class can delegate to the interface to make the code more readable. The test version runs everything on the same thread, so all coroutines execute synchronously. This is pretty good, though it means creating an abstraction for a language feature, which seems a bit much.

I had some success using a dispatcher created from an Executor which synchronously calls the code passed to it:

object DirectExecutor : Executor {
    override fun execute(command: Runnable?) {
        command?.run()
    }
}

The class constructor now takes a UI dispatcher and a background dispatcher. In the real code you pass in UI and CommonPool, while in the test you pass DirectExecutor.asCoroutineDispatcher(). This works pretty well, unless you call delay() in your coroutine, which causes the coroutine to resume on a background thread.

I think ultimately the best way would be to create a full-featured test dispatcher, which always runs code on the main thread, and handles things like starting lazily.

Anyone else got any ideas?

2 Likes

There are a few options, but why do use launch(UI) right inside doSomething? It is going to be easier to test if that was a suspend fun doSomething() = withContext(UI) { … }. Then you’d simply invoke it from your test with UI replaced by coroutineContext of your runBlocking and it would work as desired, even with delays.

1 Like

If you’re suggesting changing the class’ public API to be a suspend function, I can see a couple of problems with that.

  • You need to move the launch to some other class, which should be unit tested at some point. This seems like kicking the can down the road, since you’ll have the same problems testing that class.
  • My example is based on an Android view model with data binding that I’m working on. So the code which calls this is generated by the compiler, and I can’t stick a launch in there.

The other way to go would be something like this:

fun doSomething() = launch(UI) {
    doSomethingImpl()
}

@VisibleForTesting
suspend fun doSomethingImpl() = withContext(uiContext) { ... }

That seems like a code smell to me.

It boils down to this: I’ve got a class with a public API. For testing I shouldn’t care what the implementation is. If the class requires me to inject a dispatcher or context, that’s fine. But making me change the API just so I can test it isn’t good.

2 Likes

It occurred to me that instead of a test dispatcher, there could also be a JUnit 4 Rule or JUnit 5 Extension that forces coroutines to run synchronously. Android has the InstantTaskExecutorRule which makes the new Architecture Components (e.g. LiveData) run synchronously.

I know it is also not ideal, but you could use Robolectric for that. It simulates Android’s UI thread (along with all other Android classes).

Thanks to this answer to a related StackOverflow question. I think I’ve got a good solution.

class TestDirectContext : CoroutineDispatcher(), Delay {
    override fun scheduleResumeAfterDelay(time: Long, unit: TimeUnit, continuation: CancellableContinuation<Unit>) {
        continuation.resume(Unit)
    }

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        block.run()
    }
}

Using this as a coroutine’s context will run it on the calling thread (like the DirectExecutor from my original post) and calls to delay() don’t switch execution to a background thread (delay() also doesn’t delay, which is probably fine in a unit test).

1 Like

FYI, with Kotlin 1.3 you need to annotate this class and all unit test classes that use it with @InternalCoroutinesApi. This is because Delay is now an internal API.

As a fellow Android developer, I’ve hit the same issue. There’s a much simpler solution that requires two small changes to your initial code.

  1. Make doSomething() return the Job

    fun doSomething(): Job {
        return launch(UI) {
            val data = getDataFromNet()
            updateUI(data)
        }
    }

  1. Since you have a reference to the Job, you can call join()

@Test
fun testSomething() {
    runBlocking {
        val testObj = SomeClass(coroutineContext)
        testObj.doSomething().join()
        // validate
    }
}

For your tests, consider using : https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-test/

Also, a parent job can be passed into constructor (can be in a CoroutineScope or CoroutinesContext along with a Dispatcher) if implementing a non-suspending interface is needed. Then the test can wait for the parent job.

I mentioned (and discarded) both returning the Job and passing a parent in my original post.

The coroutines test library looks very nice, but didn’t exist when I originally posted. Going forward I would probably use that instead of my homemade dispatcher.