Unit Testing with Kotlin Coroutines: The Android Way
In our app testing strategy, Unit tests are the fundamental tests. With the Unit test, we can verify if the individual logic we have written is correct. We can run those Unit tests after each build to make sure any new changes don’t cause impacts on the existing code. Thus Unit tests help us quickly catch and fix issues before the release.
If you are new to Android Development or already have years of experience, you should be quite familiar with Kotlin now. And after the deprecation of AsyncTask from API level 30, the best option you have is to go for RxJava or Kotlin’s Coroutines for Asynchronous Programming. This article is all about Kotlin’s Coroutines and different approaches to test them. We will also explore the support libraries and best practices.
What will we learn?
- Testing suspend functions with runBlocking() and runBlockingTest()
- Test Coroutines running on Dispatchers.Main with TestCoroutineDispatcher and MainCoroutineRule
- Inject Dispatcher to test Coroutine Dispatchers other than Dispatchers.Main e.g., Dispatchers.IO
- Test LiveData using InstantTaskExecutorRule
- Observe LiveData changes with LiveDataTestUtil
Preparing our setup:
For the demonstration of the project, we will be using the following three dependencies:
//For using viewModelScope
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"//For runBlockingTest, CoroutineDispatcher etc.
testImplementation “org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2”//For InstantTaskExecutorRule
Testing suspend functions with runBlocking() and runBlockingTest()
A suspending function is a function that can be paused and resumed at a later time. Those functions can execute a long-running operation and wait for it to complete without blocking. A suspend function can only be called from another suspend function or a Coroutine.
If our app has any suspend functions, we have to call it from another suspend function or launch it from a Coroutine. We can mitigate the problem by using the
runBlocking()function. This function runs a new Coroutine and blocks the current thread interruptibly until its completion. Let’s look at an example:
From the above example, we have a
MainViewModel with a
checkSessionExpiry()function. This function checks if the user session has expired and returns an appropriate boolean value. To simulate an IO task, we have used the
delay function. Inside the
MainViewModelTestclass, we have one test function that checks if the return value is true. If you run the test it will pass, but you will notice a delay of 5 seconds. That’s because the
runBlocking()function can’t skip the
runBlockingTest() is a part of the kotlinx.coroutines.runBlocking package. This test is similar to
runBlocking()function, but it will immediately progress past delays and into a
async blocks. You can use this to write tests that execute in the presence of calls to delay without causing your test to take extra time. Our previous test will run without any delays if we launch the same test with
1. This test is marked as an ExperimentalCoroutinesApi so in the future it may change
2. Never use runBlocking() in your app. It’s meant to be used for Unit tests only
Test Coroutines running on Dispatchers.Main with TestCoroutineDispatcher and MainCoroutineRule
We had a successful Unit test with
runBlockingTest(), but have you noticed we launched both tests in the
Dispatcher.IO thread? What will happen if we use
Dispatcher.IO? Let’s see an example:
viewModelScopeis a part of the androidx.lifecycle package. This scope is tied to the
ViewModeland will be canceled when
ViewModelwill be cleared, i.e
2. By default,
viewModelScopealways runs on the main thread. But you can configure it to run on another thread by setting up a Dispatcher
If we try to run the Unit test in the
Dispatcher.Main thread, our test will fail with the following error:
Exception in thread "main @coroutine#1" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
It makes sense because we can’t use the main looper in the Unit test. Let’s try replacing the Dispatcher with a
- All consecutive usages of
Dispatchers.Mainwill use given dispatcher under the hood
- Resets the state of the
Dispatchers.Mainto the original main dispatcher
- Clean up the
TestCoroutineDispatcherto make sure no other work is running
So far, we have found ways to test our
Dispatcher.Main Coroutines. By default, Coroutines work for
Dispatcher.Main, but to test
Dispatcher.IO, we have to use the
TestCoroutineDispatcher for replacing the main dispatchers with the test dispatcher. This works correctly but doesn’t scale up very well. Whenever our test class increases, we have to use the same boilerplate again and again. To overcome this issue, we can create a custom MainCoroutineRule and add it to our test classes with
MainCoroutineRule sets the main Coroutines dispatcher to a
TestCoroutineScope for Unit testing. The
TestCoroutineScope provides control over the execution of Coroutines. All we have to is use it as a
Inject Dispatcher to test Coroutine dispatchers other than Dispatchers.Main e.g., Dispatchers.IO
Previously, we explored ways to test Coroutines for both
Dispatchers.IO. But, what will happen when we have to replace a dispatcher dynamically for a Unit test? Let’s consider the following example:
Here, we are setting the
TestCorotutineDispatcher as the main Dispatcher via
MainCoroutineRule which takes control over the MainDispatcher, but we still have no control over the execution of the Coroutine launched via
viewModelScope.launch(Dispatchers.IO). Also, if we run the test now, It will fail with assertion error because our test will run on the other thread which is different from the
ViewModelthread. Now, how should we resolve this?
According to Google:
Dispatchersshould be injected into your
ViewModelsso you can properly test. Passing the
Dispatchervia the constructor would make sure that your test and production code use the same dispatcher.
ViewModel and the test class should look this:
Run the test, and it should be successful now.
Test LiveData using InstantTaskExecutorRule
So far, we have covered everything required for testing our Coroutines. As a bonus, I would like to discuss more testing the
LiveData. Let’s start with an example:
It looks like a perfect test to proceed. But if we run the Unit test, there will be an error:
Exception in thread "main @coroutine#2" java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details. at android.os.Looper.getMainLooper(Looper.java)
LiveData is a part of the Android lifecycle, and it needs to access the main looper to proceed. Fortunately, we only need a single line of code to resolve this. Just add
InstantTaskExecutorRule as a
@Rule from the androidx.arch.core.executor.testing package. Now re-run the test, and it should be successful.
JUnitTest Rule that swaps the background executor used by the Architecture Components with a different one that executes each task synchronously.
Observe LiveData changes with LiveDataTestUtil
If you need to observe
LiveData changes, then you can use an extension function called LiveDataTestUtil. It will help you observe
LiveData changes without any extra effort.
If you use Dagger in your project, you can check out another article to explore the Unit testing technique with Dagger.
Unit Testing with Dagger 2: Brewing a potion with Fakes, Mocks, and Custom Rules in Android
If you have Dagger in your project but never used it for testing, it’s high time to give it a try. Dagger makes your…
Also, check out the article about generating Unit test coverage reports with JaCoCo.
Multi-Module, Multi-Flavored Test Coverage with JaCoCo in Android
A detailed guide to configuring your JaCoCo script for running test coverage into a multi-module, multi-flavored…
I wrote this article based on the ground-up tips from this article.