The Startup
Published in

The Startup

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?

  1. Testing suspend functions with runBlocking() and runBlockingTest()
  2. Test Coroutines running on Dispatchers.Main with TestCoroutineDispatcher and MainCoroutineRule
  3. Inject Dispatcher to test Coroutine Dispatchers other than Dispatchers.Main e.g., Dispatchers.IO
  4. Test LiveData using InstantTaskExecutorRule
  5. 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
testImplementation “androidx.arch.core:core-testing:2.1.0”

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.

runBlocking()

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 delayoperation.

runBlockingTest()

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 alaunch and 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 runBlockingTest().

Notes:

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

TestCoroutineDispatcher

We had a successful Unit test with runBlocking()and runBlockingTest(), but have you noticed we launched both tests in the Dispatcher.IO thread? What will happen if we use Dispatcher.Maininstead of Dispatcher.IO? Let’s see an example:

Notes:

1. viewModelScope is a part of the androidx.lifecycle package. This scope is tied to the ViewModel and will be canceled when ViewModelwill be cleared, i.e ViewModel.onCleared() is called.

2. By default, viewModelScope always 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 TestCoroutineDispatcher.

Code Breakdown:

  1. All consecutive usages of Dispatchers.Main will use given dispatcher under the hood
  2. Resets the state of theDispatchers.Main to the original main dispatcher
  3. Clean up the TestCoroutineDispatcher to make sure no other work is running

MainCoroutineRule

So far, we have found ways to test our Dispatcher.IOand 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 @Rule annotation.

MainCoroutineRule sets the main Coroutines dispatcher to a TestCoroutineScope for Unit testing. TheTestCoroutineScope provides control over the execution of Coroutines. All we have to is use it as a@Rule

Inject Dispatcher to test Coroutine dispatchers other than Dispatchers.Main e.g., Dispatchers.IO

Previously, we explored ways to test Coroutines for both Dispatchers.Main and 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:

Dispatchers should be injected into your ViewModels so you can properly test. Passing the Dispatcher via the constructor would make sure that your test and production code use the same dispatcher.

Our updated 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)

That’s because 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.

InstantTaskExecutorRuleis a 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.

--

--

--

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +756K followers.

Recommended from Medium

LiveData vs SharedFlow and StateFlow in MVVM and MVI Architecture

🔧⚡Fixing ConstraintLayout & Guideline AssertionError Exception in Android 📱 💻

Paging 3 — Loading States, Separators, refresh(), retry()

Android Intent security vulnerabilities

Making Screens Adaptive to different screen sizes in the most easy way

Boost Your Kotlin Productivity With Extensions and Higher-Order Functions

Android Kotlin Base64 Encode/Decode

Responsive Routing in Flutter. MaterialPageRoute vs CupertinoPageRoute in one App.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Prokash Sarkar

Prokash Sarkar

An Android enthusiastic. Currently pursuing a perfect blend of style and function for a wide range of Android Applications.

More from Medium

Looking Back Into HandlerThread Android Internal💫

Understanding MockK Kotlin

DiffUtil in multiple columns list with different items

Zip, combineLatest, debounce etc in Coroutines?