Android Unit Testing with Coroutines

Adalberto Plaza
Degoo
Published in
3 min readJan 26, 2021

If you are here, I assume you are already familiar with Unit Testing on Android, but you are wondering why your tests fail when running coroutines. If you need more info about Unit Testing, pelase refer to my testing series: https://medium.com/@adalberto.plaza/android-testing-kotlin-introduction-4f0b1dcf387b

When comes to testing functions which are using coroutines, the main problem is that they are handling thread switching, asynchronous calls and/or using different dispatchers. However, our test functions are run without being aware of the concurrency because they run over a single thread. Furthermore, there are a specific dispatcher (MainDispatcher) using the Android UI Thread, which is not available in unit tests.

Fortunately, fixing these issues with coroutines is pretty simple since they actually provides some easy mechanisms developed for testing purposes.

Depedencies

We need a couple of extra dependencies for testing:

"androidx.arch.core:core-testing:x.x.x"
"org.jetbrains.kotlinx:kotlinx-coroutines-test:x.x.x"

The first one will provide a rule to be able to instantly run tasks in a synchronous executor while the second one provides the different tools we are about to use to run coroutines in tests.

Rules

@get:Rule
val testInstantTaskExecutorRule: TestRule = InstantTaskExecutorRule()

@get:Rule
val mainCoroutineScopeRule: MainCoroutineScopeRule = MainCoroutineScopeRule()

Here is the rule we are talking about. It just specify to the test class to use the instant task executor instead of waiting for background jobs in Architecture Components.
The second rule is used to replace the Main dispatcher by a testing one. So we can test classes which are using it without having to do anything else. For example ViewModels.
Notice that, MainCoroutineScopeRule is a customised TestWatcher, so we can replace the MainDispatcher there.

Testing ViewModels

Let’s see a complete example, following my previous MVVM and coroutines post.

So the test should look like

As you can see the test is pretty much the same than a regular tests with no coroutines. We have just added the special rules to replace the Dispatcher.Main that viewModelScope is using, so it’s run on the test one.
The code is self-explicative, but we have just prepared the mock data we need and checked the UIStates are empty. After that, we have called the main function on the ViewModel and checked the correct UIStates again.

Testing suspend functions

If you haven’t “cheated” with my code (as I did) probably you observed an error in compile time. This is because actually getDataUseCase.call() was a suspend function and we are not calling it from a CoroutineScope in the test.

How can we solve that? pretty easy!

Do yu remember that we have a MainCoroutineScopeRule right? Let’s use that scope to call suspend functions in tests! (See Bonus example)

Bonus

Probably in suspend functions you are specifying the dispatcher in which you want to run the code. Well, if you do that, then the code inside it is not testable because we are replacing the Main dispatcher but no the other ones. Instead of that, is better if you follow a better pattern and inject the dispatchers to make them replaceable in tests. (And also to follow better practises). Let’s see an example of how to properly call suspend functions in tests.

Here is the DispatcherProvider to inject the dispathcer in our classes.

And this is how we could use it in a repository which is using the context of the io scope. So instead of using withContext(Dispatchers.IO) { ... } we use the provider.

And finally, this is how the test will look. Notice that:

  • The test is run in mainCoroutineScopeRule.runBlockingTestto be able to call suspend funtions. Besides it blocks the thread until the test has finished.
  • We are injecting a testing dispatcher provider which ALWAYS returns the testDispatcher

Conclusion

Testing coroutines could seem like a complex task to do, but after you succeed with your first test, you can see the configuration and the development of tests is pretty similar to how it it with regular calls!

--

--