Kotlin Coroutines + Android: How to Unit Test LifecycleCoroutineScope
Unless you’ve been asleep for the past six months, you probably already know about Kotlin Coroutines, and how Google has gone all in on developing first-class support for them, as an asynchronous work management system.
You might even know about LifecycleCoroutineScope, and how it auto-magically both suspends and cancels your coroutines based on the lifecycle of the Activities or Fragments in your app.
But there’s one last tricky little mystery to solve: How the heck do you Unit Test these things?
The good people at JetBrains have released a library specifically for this, but simply knowing that is not gonna get you there. Luckily, again, I’ve figured it all out, and I have a step by step guide to Unit Testing your app, using Coroutines, LifecycleCoroutineScope and the Kotlin Coroutines Test library.
Dependencies
First things first, you need to update your dependencies. If you’re using out of date versions of things, you’re gonna have a bad time.
Infrastructure
Next we need to install some infrastructure so that we can swap out real Coroutine scopes for mock ones when we test. So the first thing we’re going to do is create an abstraction on top of CoroutineScope:
Next, we need to create two implementations, one to use in our app, which will actually delegate to LifecycleCoroutineScope, and one for our unit tests, which will delegate to the TestCoroutineScope provided by the kotlinx-coroutines-test library:
As you can see, in LifecycleManagedCoroutineScope, the launch function calls through to launchWhenStarted, to take advantage of that scope’s auto-suspend and cancel functionality, but the Test version just calls launch.
Write the Test
Finally it’s time to actually test stuff. Using dependency injection we have all our presenters (or models) use a ManagedScope object to launch coroutines, and we’ll provide a LifecycleManagedCoroutineScope as the implementation. That way, for the test, we can easily override that scope with the test version:
So, in the test class above, we’re using the TestScope class that we wrote earlier, and passing in a TestCoroutineDispatcher (conveniently provided by the kotlinx-coroutines-test library) as the context.
We’re also making sure to set our Main dispatcher to the TestCoroutineDispatcher as well, and also to reset the Main dispatcher in the teardown. (A lot of this can probably be encapsulated in a test Rule.)
In the actual test, notice that we’re using the runBlockingTest closure (again, provided by the kotlinx-coroutines-test library). This allows us to call suspend functions, and verify their results. In this example, the model.loadData(false) function is a suspend function, meaning that it can only be called (or in this case, mocked) from a coroutine.
Summary
Coroutines are still pretty new, and this is only one potential solution to a problem that may very well get solved inside the framework or in the supporting libraries as Coroutines mature. At the very least, this method will get you functional for now; just make sure to stay up to date on new library releases, and someday soon this might all be cleaned up.