(Deprecated) Advanced Android in Kotlin 05.3: Testing Coroutines and Jetpack integrations

1. Welcome

Introduction

This third testing codelab is a survey of additional testing topics, including:

  • Coroutines, including view model scoped coroutines
  • Room
  • Databinding
  • End-to-End tests

What you should already know

You should be familiar with:

What you'll learn

  • How to test coroutines, including view model scoped coroutines.
  • How to test simple error edge cases.
  • How to test Room.
  • How to test data binding with Espresso.
  • How to write end-to-end tests.
  • How to test global app navigation.

You will use:

What you'll do

  • Write ViewModel integration tests that test code using viewModelScope.
  • Pause and resume coroutine execution for testing.
  • Modify a fake repository to support error testing.
  • Write DAO unit tests.
  • Write local data source integration tests.
  • Write end-to-end tests that include coroutine and data binding code.
  • Write global app navigation tests.

2. App overview

In this series of codelabs, you'll be working with the TO-DO Notes app. The app allows you to write down tasks to complete and displays them in a list. You can then mark them as completed or not, filter them, or delete them.

441dc71d6f7d5807.gif

This app is written in Kotlin, has a few screens, uses Jetpack components, and follows the architecture from a Guide to app architecture. Learning how to test this app will enable you to test apps that use the same libraries and architecture.

Download the Code

To get started, download the code:

Alternatively, you can clone the Github repository for the code:

$ git clone https://github.com/google-developer-training/advanced-android-testing.git
$ cd android-testing
$ git checkout end_codelab_2

Take a moment to familiarize yourself with the code, following the instructions below.

Step 1: Run the sample app

Once you've downloaded the TO-DO app, open it in Android Studio and run it. It should compile. Explore the app by doing the following:

  • Create a new task with the plus floating action button. Enter a title first, then enter additional information about the task. Save it with the green check FAB.
  • In the list of tasks, click on the title of the task you just completed and look at the detail screen for that task to see the rest of the description.
  • In the list or on the detail screen, check the checkbox of that task to set its status to Completed.
  • Go back to the tasks screen, open the filter menu, and filter the tasks by Active and Completed status.
  • Open the navigation drawer and click Statistics.
  • Got back to the overview screen, and from the navigation drawer menu, select Clear completed to delete all tasks with the Completed status

483916536f10c42a.png

Step 2: Explore the sample app code

The TO-DO app is based off of the Architecture Blueprints testing and architecture sample. The app follows the architecture from a Guide to app architecture. It uses ViewModels with Fragments, a repository, and Room. If you're familiar with any of the below examples, this app has a similar architecture:

It is more important that you understand the general architecture of the app than have a deep understanding of the logic at any one layer.

f2e425a052f7caf7.png

Here's the summary of packages you'll find:

Package: com.example.android.architecture.blueprints.todoapp

.addedittask

The add or edit a task screen: UI layer code for adding or editing a task.

.data

The data layer: This deals with the data layer of the tasks. It contains the database, network, and repository code.

.statistics

The statistics screen: UI layer code for the statistics screen.

.taskdetail

The task detail screen: UI layer code for a single task.

.tasks

The tasks screen: UI layer code for the list of all tasks.

.util

Utility classes: Shared classes used in various parts of the app, e.g. for the swipe refresh layout used on multiple screens.

Data layer (.data)

This app includes a simulated networking layer, in the remote package, and a database layer, in the local package. For simplicity, in this project the networking layer is simulated with just a HashMap with a delay, rather than making real network requests.

The DefaultTasksRepository coordinates or mediates between the networking layer and the database layer and is what returns data to the UI layer.

UI layer ( .addedittask, .statistics, .taskdetail, .tasks)

Each of the UI layer packages contains a fragment and a view model, along with any other classes that are required for the UI (such as an adapter for the task list). The TaskActivity is the activity that contains all of the fragments.

Navigation

Navigation for the app is controlled by the Navigation component. It is defined in the nav_graph.xml file. Navigation is triggered in the view models using the Event class; the view models also determine what arguments to pass. The fragments observe the Events and do the actual navigation between screens.

3. Task: Introduction to and review of testing coroutines

Code executes either synchronously or asynchronously.

  • When code is running synchronously, a task completely finishes before execution moves to the next task.
  • When code is running asynchronously, tasks run in parallel.

The scheme displays synchronous code.

The scheme displays asynchronous code.

Asynchronous code is almost always used for long-running tasks, such as network or database calls. It can also be difficult to test. There are two common reasons for this:

  • Asynchronous code tends to be non-deterministic. What this means is that if a test runs operations A and B in parallel, multiple times, sometimes A will finish first, and sometimes B. This can cause flaky tests (tests with inconsistent results).

4a3a1e86d86365ad.png

  • When testing, you often need to ensure some sort of synchronization mechanism for asynchronous code. Tests run on a testing thread. As your test runs code on different threads, or makes new coroutines, this work is started asynchronously, seperate from the test thread. Meanwhile the test coroutine will keep executing instructions in parallel. The test might finish before either of the fired-off tasks finish. 9f5df9c30b2b48f4.pngSynchronization mechanisms are ways to tell the test execution to "wait" until the asynchronous work finishes.

810231c26c4c77b2.png

In Kotlin, a common mechanism for running code asynchronously is coroutines. When testing asynchronous code, you need to make your code deterministic and provide synchronization mechanisms. The following classes and methodologies help with that:

  • Using runBlockingTest or runBlocking.
  • Using TestCoroutineDispatcher for local tests.
  • Pausing coroutine execution to test the state of the code at an exact place in time.

You will start by exploring the difference between runBlockingTest and runBlocking.

Step 1: Observe how to run basic coroutines in tests

To test code that includes suspend functions, you need to do the following:

  1. Add the kotlinx-coroutines-test test dependency to your app's build.gradle file.
  2. Annotate the test class or test function with @ExperimentalCoroutinesApi.
  3. Surround the code with runBlockingTest, so that your test waits for the coroutine to finish.

Let's look at an example.

  1. Open your app's build.gradle file.
  2. Find the kotlinx-coroutines-test dependency (this is provided for you):

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

kotlinx-coroutines-test is an experimental library for testing coroutines. It includes utilities for testing coroutines, including runBlockingTest.

You must use runBlockingTest whenever you want to run a coroutine from a test. Usually, this is when you need to call a suspend function from a test.

  1. Take a look at this example from TaskDetailFragmentTest.kt. Pay attention to the lines that say //LOOK HERE:

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
@ExperimentalCoroutinesApi // LOOK HERE
class TaskDetailFragmentTest {

    //... Setup and teardown

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{ // LOOK HERE
        // GIVEN - Add active (incomplete) task to the DB.
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask) // LOOK HERE Example of calling a suspend function

        // WHEN - Details fragment launched to display task.
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen.
        // Make sure that the title/description are both shown and correct.
        // Lots of Espresso code...
    }

    // More tests...
}

To use runBlockingTest you:

  • Annotate the function or class with @ExperimentalCoroutinesApi.
  • Wrap the code calling the suspend function with runBlockingTest.

When you use any functions from kotlinx-coroutines-test, annotate the class or function with @ExperimentalCoroutinesApi since kotlinx-coroutines-test is still experimental and the API might change. If you don't do this, you'll get a lint warning.

runBlockingTest is used in the above code because you are calling repository.saveTask(activeTask), which is a suspend function.

runBlockingTest handles both running the code deterministically and providing a synchronization mechanism. runBlockingTest takes in a block of code and blocks the test thread until all of the coroutines it starts are finished. It also runs the code in the coroutines immediately (skipping any calls to delay) and in the order they are called–-in short, it runs them in a deterministic order.

runBlockingTest essentially makes your coroutines run like non-coroutines by giving you a coroutine context specifically for test code.

You do this in your tests because it's important that the code runs in the same way every single time (synchronous and deterministic).

Step 2: Observe runBlocking in Test Doubles

There is another function, runBlocking, which is used when you need to use coroutines in your test doubles as opposed to your test classes. Using runBlocking looks very similar to runBlockingTest, as you wrap it around a code block to use it.

  1. Take a look at this example from FakeTestRepository.kt. Note that since runBlocking is not part of the kotlinx-coroutines-test library, you do not need to use the ExperimentalCoroutinesApi annotation.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // More code...

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() } // LOOK HERE
        return observableTasks
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }
    
    // More code...
}

Similar to runBlockingTest, runBlocking is used here because refreshTasks is a suspend function.

runBlocking vs. runBlockingTest

Both runBlocking and runBlockingTest block the current thread and wait until any associated coroutines launched in the lambda complete.

In addition, runBlockingTest has the following behaviors meant for testing:

  1. It skips delay, so your tests run faster.
  2. It adds testing related assertions to the end of the coroutine. These assertions fail if you launch a coroutine and it continues running after the end of the runBlocking lambda (which is a possible coroutine leak) or if you have an uncaught exception.
  3. It gives you timing control over the coroutine execution.

So why use runBlocking in your test doubles, like FakeTestRepository? Sometimes you will need a coroutine for a test double, in which case you do need to block the current thread. This is, so that when your test doubles are used in a test case, the thread blocks and allows the coroutine to finish before the test does. Test doubles, though, aren't actually defining a test case, so they don't need and shouldn't use all of the test specific features of runBlockingTest.

In summary:

  • Tests require deterministic behavior so they aren't flaky.
  • "Normal" coroutines are non-deterministic because they run code asynchronously.
  • kotlinx-coroutines-test is the gradle dependency for runBlockingTest.
  • Writing test classes, meaning classes with @Test functions, use runBlockingTest to get deterministic behavior.
  • Writing test doubles, use runBlocking.

4. Task: Coroutines and ViewModels

In this step you'll learn how to test view models that use coroutines.

All coroutines require a CoroutineScope. Coroutine scopes control the lifetimes of coroutines. When you cancel a scope (or technically, the coroutine's Job, which you can learn more about here), all of the coroutines running in the scope are cancelled.

Since you might start long running work from a view model, you'll often find yourself creating and running coroutines inside view models. Normally, you'd need to create and configure a new CoroutineScope manually for each view model to run any coroutines. This is a lot of boilerplate code. To avoid this, lifecycle-viewmodel-ktx provides an extension property called viewModelScope.

viewModelScope is a CoroutineScope associated with each view model. viewModelScope is configured for use in that particular ViewModel. What this means specifically is that:

  • The viewModelScope is tied to the view model such that when the view model is cleaned up (i.e. onCleared is called), the scope is cancelled. This ensures that when your view model goes away, so does all the coroutine work associated with it. This avoids wasted work and memory leaks.
  • The viewModelScope uses the Dispatchers.Main coroutine dispatcher. A CoroutineDispatcher controls how a coroutine runs, including what thread the coroutine code runs on. Dispatcher.Main puts the coroutine on the UI or main thread. This makes sense as a default for ViewModel coroutines, because often, view models manipulate the UI.

This works well in production code. But for local tests (tests that run on your local machine in the test source set), the usage of Dispatcher.Main causes an issue: Dispatchers.Main uses Android's Looper.getMainLooper(). The main looper is the execution loop for a real application. The main looper is not available (by default) in local tests, because you're not running the full application.

To address this, use the method setMain() (from kotlinx.coroutines.test) to modify Dispatchers.Main to use TestCoroutineDispatcher. TestCoroutineDispatcher is a dispatcher specifically meant for testing.

Next, you will write tests for view model code that uses viewModelScope.

Step 1: Observe Dispatcher.Main causing an error

Add a test which checks that when a task is completed, the snackbar shows the correct completion message.

  1. Open test > tasks > TasksViewModelTest.

6818c266d2e6853e.png

  1. Add this new test method:

TasksViewModelTest.kt

@Test
fun completeTask_dataAndSnackbarUpdated() {
    // Create an active task and add it to the repository.
    val task = Task("Title", "Description")
    tasksRepository.addTasks(task)

    // Mark the task as complete task.
    tasksViewModel.completeTask(task, true)

    // Verify the task is completed.
   assertThat(tasksRepository.tasksServiceData[task.id]?.isCompleted, `is`(true))

    // Assert that the snackbar has been updated with the correct text.
    val snackbarText: Event<Int> =  tasksViewModel.snackbarText.getOrAwaitValue()
    assertThat(snackbarText.getContentIfNotHandled(), `is`(R.string.task_marked_complete))
}
  1. Run this test. Observe that it fails with the following error:

"Exception in thread "main" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used."

This error states that the Dispatcher.Main has failed to initialize. The underlying reason (not explained in the error) is the lack of Android's Looper.getMainLooper(). The error message does tell you to use Dispatcher.setMain from kotlinx-coroutines-test. Go ahead and do just that!

Step 2: Replace Dispatcher.Main with TestCoroutineDispatcher

TestCoroutineDispatcher is a coroutine dispatcher meant for testing. It executes tasks immediately and gives you control over the timing of coroutine execution in tests, such as allowing you to pause and restart coroutine execution.

  1. In TasksViewModelTest, create a TestCoroutineDispatcher as a val called testDispatcher.

Use testDispatcher instead of the default Main dispatcher.

  1. Create a @Before method that calls Dispatchers.setMain(testDispatcher) before every test.
  2. Create an @After method that cleans everything up after running each test by calling Dispatchers.resetMain()and then testDispatcher.cleanupTestCoroutines().

Here's what this code looks like:

TasksViewModelTest.kt

@ExperimentalCoroutinesApi
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()

@ExperimentalCoroutinesApi
@Before
fun setupDispatcher() {
    Dispatchers.setMain(testDispatcher)
}

@ExperimentalCoroutinesApi
@After
fun tearDownDispatcher() {
    Dispatchers.resetMain()
    testDispatcher.cleanupTestCoroutines()
}
  1. Run your test again. It now passes!

Step 3: Add MainCoroutineRule

If you're using coroutines in your app, any local test that involves calling code in a view model is highly likely to call code which uses viewModelScope. Instead of copying and pasting the code to set up and tear down the TestCoroutineDispatcher into each test class, you can make a custom JUnit rule to avoid this boilerplate code.

JUnit rules are classes where you can define generic testing code that can execute before, after, or during a test–-it's a way to take your code that would have been in @Before and @After, and put it in a class where it can be reused.

Make a JUnit rule now.

  1. Create a new class called MainCoroutineRule.kt in the root folder of the test source set:

72c1d0a7ae5c2c04.png

  1. Copy the following code to MainCoroutineRule.kt:

MainCoroutineRule.kt

@ExperimentalCoroutinesApi
class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()):
   TestWatcher(),
   TestCoroutineScope by TestCoroutineScope(dispatcher) {
   override fun starting(description: Description?) {
       super.starting(description)
       Dispatchers.setMain(dispatcher)
   }

   override fun finished(description: Description?) {
       super.finished(description)
       cleanupTestCoroutines()
       Dispatchers.resetMain()
   }
}

Some things to notice:

  • MainCoroutineRule extends TestWatcher, which implements the TestRule interface. This is what makes MainCoroutineRule a JUnit rule.
  • The starting and finished methods match what you wrote in your @Before and @After functions. They also run before and after each test.
  • MainCoroutineRule also implements TestCoroutineScope, to which you pass in the TestCoroutineDispatcher. This gives MainCoroutineRule the ability to control coroutine timing (using the TestCoroutineDispatcher you pass in). You'll see an example of this in the next step.

Step 4: Use your new Junit rule in a test

  1. Open TasksViewModelTest.
  2. Replace testDispatcher and your @Before and @After code with the new MainCoroutineRule JUnit rule:

TasksViewModelTest.kt

// REPLACE@ExperimentalCoroutinesApi
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()

@ExperimentalCoroutinesApi
@Before
fun setupDispatcher() {
    Dispatchers.setMain(testDispatcher)
}

@ExperimentalCoroutinesApi
@After
fun tearDownDispatcher() {
    Dispatchers.resetMain()
    testDispatcher.cleanupTestCoroutines()
}
// WITH
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()

Notice: To use the JUnit rule, you instantiate the rule and annotate it with @get:Rule.

  1. Run completeTask_dataAndSnackbarUpdated, and it should work exactly the same!

Step 5: Use MainCoroutineRule for repository testing

In the previous codelab, you learned about dependency injection. This allows you to replace the production versions of classes with test versions of classes in your tests. Specifically, you used constructor dependency injection. Here's an example from DefaultTasksRepository:

DefaultTasksRepository.kt

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) : TasksRepository { ... }

The above code injects a local and remote data source, as well as a CoroutineDispatcher. Because the dispatcher is injected, you can use TestCoroutineDispatcher in your tests. Injecting the CoroutineDispatcher, as opposed to hard coding the dispatcher, is a good habit when using coroutines.

Let's use the injected TestCoroutineDispatcher in your tests.

  1. Open test > data > source > DefaultTasksRepositoryTest.kt

d95afb8e34702b79.png

  1. Add the MainCoroutineRule inside the DefaultTasksRepositoryTest class:

DefaultTasksRepositoryTest.kt

// Set the main coroutines dispatcher for unit testing.
@ExperimentalCoroutinesApi
@get:Rule
var mainCoroutineRule = MainCoroutineRule()
  1. Use Dispatcher.Main, instead of Dispatcher.Unconfined when defining your repository under test. Similar to TestCoroutineDispatcher, Dispatchers.Unconfined executes tasks immediately. But, it doesn't include all of the other testing benefits of TestCoroutineDispatcher, such as being able to pause execution:

DefaultTasksRepositoryTest.kt

@Before
fun createRepository() {
    tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
    tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
    // Get a reference to the class under test.
    tasksRepository = DefaultTasksRepository(
    // HERE Swap Dispatcher.Unconfined
        tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Main
    )
}

In the above code, remember that MainCoroutineRule swaps the Dispatcher.Main for a TestCoroutineDispatcher.

Generally, only