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?