Advanced Android in Kotlin 05.2: Introduction to Test Doubles and Dependency Injection

1. Welcome

Introduction

This second testing codelab is all about test doubles: when to use them in Android, and how to implement them using dependency injection, the Service Locator pattern, and libraries. In doing this, you'll learn how to write:

  • Repository unit tests
  • Fragments and viewmodel integration tests
  • Fragment navigation tests

What you should already know

You should be familiar with:

What you'll learn

  • How to plan a testing strategy
  • How to create and use test doubles, namely fakes and mocks
  • How to use manual dependency injection on Android for unit and integration tests
  • How to apply the Service Locator Pattern
  • How to test repositories, fragments, view models and the Navigation component

You will use the following libraries and code concepts:

What you'll do

  • Write unit tests for a repository using a test double and dependency injection.
  • Write unit tests for a view model using a test double and dependency injection.
  • Write integration tests for fragments and their view models using Espresso UI testing framework.
  • Write navigation tests using Mockito and Espresso.

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.

e490df637e1bf10c.gif

This app is written in Kotlin, has a few screens, uses Jetpack components and follows the architecture from a Guide to app architecture. By learning how to test this app, you'll be able 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_1

You can browse the code in the android-testing Github repository.

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. Concept: Testing Strategy

In this codelab, you will learn how to test repositories, view models, and fragments using test doubles and dependency injection. Before you dive into what those are, it's important to understand the reasoning that will guide what and how you will write these tests.

This section covers some best practices of testing in general, as they apply to Android.

The Testing Pyramid

When thinking about a testing strategy, there are three related testing aspects:

  • Scope—How much of the code does the test touch? Tests can run on a single method, across the entire application, or somewhere in between.
  • Speed—How fast does the test run? Test speeds can vary from milli-seconds to several minutes.
  • Fidelity—How "real-world" is the test? For example, if part of the code you're testing needs to make a network request, does the test code actually make this network request, or does it fake the result? If the test actually talks with the network, this means it has higher fidelity. The trade-off is that the test could take longer to run, could result in errors if the network is down, or could be costly to use.

There are inherent trade-offs between these aspects. For example, speed and fidelity are a trade-off—the faster the test, generally, the less fidelity, and vice versa. One common way to divide automated tests is into these three categories:

  • Unit tests—These are highly focused tests that run on a single class, usually a single method in that class. If a unit test fails, you can know exactly where in your code the issue is. They have low fidelity since in the real world, your app involves much more than the execution of one method or class. They are fast enough to run every time you change your code. They will most often be locally run tests (in the test source set). Example: Testing single methods in view models and repositories.
  • Integration tests—These test the interaction of several classes to make sure they behave as expected when used together. One way to structure integration tests is to have them test a single feature, such as the ability to save a task. They test a larger scope of code than unit tests, but are still optimized to run fast, versus having full fidelity. They can be run either locally or as instrumentation tests, depending on the situation. Example: Testing all the functionality of a single fragment and view model pair.
  • End to end tests (E2e)—Test a combination of features working together. They test large portions of the app, simulate real usage closely, and therefore are usually slow. They have the highest fidelity and tell you that your application actually works as a whole. By and large, these tests will be instrumented tests (in the androidTest source set) Example: Starting up the entire app and testing a few features together.

The suggested proportion of these tests is often represented by a pyramid, with the vast majority of tests being unit tests.

7017a2dd290e68aa.png

Architecture and Testing

Your ability to test your app at all the different levels of the testing pyramid is inherently tied to your app's architecture. For example, an extremely poorly-architected application might put all of its logic inside one method. You might be able to write an end to end test for this, since these tests tend to test large portions of the app, but what about writing unit or integration tests? With all of the code in one place, it's hard to test just the code related to a single unit or feature.

A better approach would be to break down the application logic into multiple methods and classes, allowing each piece to be tested in isolation. Architecture is a way to divide up and organize your code, which allows easier unit and integration testing. The TO-DO app that you'll be testing follows a particular architecture:

f2e425a052f7caf7.png

In this lesson, you'll see how to test parts of the above architecture, in proper isolation:

  1. First you'll unit test the repository.
  2. Then you'll use a test double in the view model, which is necessary for unit testing and integration testing the view model.
  3. Next, you'll learn to write integration tests for fragments and their view models.
  4. Finally, you'll learn to write integration tests that include the Navigation component.

End to end testing will be covered in the next lesson.

4. Task: Make a Fake Data Source

When you write a unit test for a part of a class (a method or a small collection of methods), your goal is to only test the code in that class.

Testing only code in a specific class or classes can be tricky. Let's look at an example. Open the data.source.DefaultTasksRepository class in the main source set. This is the repository for the app, and is the class which you will be writing unit tests for next.

Your goal is to test only the code in that class. Yet, DefaultTasksRepository depends on other classes, such as TasksLocalDataSource and TasksRemoteDataSource, to function. Another way to say this is that TasksLocalDataSource and TasksRemoteDataSource are dependencies of DefaultTasksRepository.

So every method in DefaultTasksRepository calls methods on data source classes, which in turn call methods in other classes to save information to a database or communicate with the network.

518a4ea76fcb835a.png

For example, take a look at this method in DefaultTasksRepo.

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks is one of the most "basic" calls you might make to your repository. This method includes reading from an SQLite database and making network calls (the call to updateTasksFromRemoteDataSource). This involves a lot more code than just the repository code.

Here are some more specific reasons why testing the repository is hard:

  • You need to deal with thinking about creating and managing a database to do even the simplest tests for this repository. This brings up questions like "should this be a local or instrumented test?" and if you should be using AndroidX Test to get a simulated Android environment.
  • Some parts of the code, such as networking code, can take a long time to run, or occasionally even fail, creating long running, flaky tests.
  • Your tests could lose their ability to diagnose which code is at fault for a test failure. Your tests could start testing non-repository code, so, for example, your supposed "repository" unit tests could fail because of an issue in some of the dependant code, such as the database code.

Test Doubles

The solution to this is that when you're testing the repository, don't use the real networking or database code, but instead use a test double. A test double is a version of a class crafted specifically for testing. It is meant to replace the real version of a class in tests. It's similar to how a stunt double is an actor who specializes in stunts, and replaces the real actor for dangerous actions.

Here are some types of test doubles:

Fake

A test double that has a "working" implementation of the class, but it's implemented in a way that makes it good for tests but unsuitable for production.

Mock

A test double that tracks which of its methods were called. It then passes or fails a test depending on whether it's methods were called correctly.

Stub

A test double that includes no logic and only returns what you program it to return. A StubTaskRepository could be programmed to return certain combinations of tasks from getTasks for example.

Dummy

A test double that is passed around but not used, such as if you just need to provide it as a parameter. If you had a NoOpTaskRepository, it would just implement the TaskRepository with no code in any of the methods.

Spy

A test double which also keeps tracks of some additional information; for example, if you made a SpyTaskRepository, it might keep track of the number of times the addTask method was called.

For more information on test doubles, check out Testing on the Toilet: Know Your Test Doubles.

The most common test doubles used in Android are Fakes and Mocks.

In this task, you're going to create a FakeDataSource test double to unit test DefaultTasksRepository decoupled from the actual data sources.

Step 1: Create the FakeDataSource class

In this step you are going to create a class called FakeDataSouce, which will be a test double of a LocalDataSource and RemoteDataSource.

  1. In the test source set, right click select New -> Package.

efdc92ba8079ed1.png

  1. Make a data package with a source package inside.
  2. Create a new class called FakeDataSource in the data/source package.

46428a328ea88457.png

Step 2: Implement TasksDataSource Interface

To be able to use your new class FakeDataSource as a test double, it must be able to replace the other data sources. Those data sources are TasksLocalDataSource and TasksRemoteDataSource.

688533d909b2330b.png

  1. Notice how both of these implement the TasksDataSource interface.
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Make FakeDataSource implement TasksDataSource:
class FakeDataSource : TasksDataSource {

}

Android Studio will complain that you haven't implemented required methods for TasksDataSource.

  1. Use the quick-fix menu and select Implement members.

890b25398497ec8d.png

  1. Select all of the methods and press OK.

61433018aef0bb29.png

Step 3: Implement the getTasks method in FakeDataSource

FakeDataSource is a specific type of test double called a fake. A fake is a test double that has a "working" implementation of the class, but it's implemented in a way that makes it good for tests but unsuitable for production. "Working" implementation means that the class will produce realistic outputs given inputs.

For example, your fake data source won't connect to the network or save anything to a database—instead it will just use an in-memory list. This will "work as you might expect" in that methods to get or save tasks will return expected results, but you could never use this implementation in production, because it's not saved to the server or a database.

A FakeDataSource

  • lets you test the code in DefaultTasksRepository without needing to rely on a real database or network.
  • provides a "real-enough" implementation for tests.
  1. Change the FakeDataSource constructor to create a var called tasks that is a MutableList<Task>? with a default value of an empty mutable list.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }

This is the list of tasks that "fakes" being a database or server response. For now, the goal is to test the repository's getTasks method. This calls the data source's getTasks, deleteAllTasks and saveTask methods.

Write a fake version of these methods:

  1. Write getTasks: If tasks isn't null, return a Success result. If tasks is null, return an Error result.
  2. Write deleteAllTasks: clear the mutable tasks list.
  3. Write saveTask: add the task to the list.

Those methods, implemented for FakeDataSource, look like the code below.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

Here are the import statements if needed:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

This is similar to how the actual local and remote data sources work.

5. Task: Write a Test Using Dependency Injection

In this step, you're going to use a technique called manual dependency injection so that you can use the fake test double you just created.

The main issue is that you have a FakeDataSource, but it's unclear how you use it in the tests. It needs to replace the TasksRemoteDataSource and the TasksLocalDataSource, but only in the tests. Both the TasksRemoteDataSource and TasksLocalDataSource are dependencies of DefaultTasksRepository, meaning that DefaultTasksRepositories requires or "depends" on these classes to run.

Right now, the dependencies are constructed inside the init method of DefaultTasksRepository.

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

Because you are creating and assigning taskLocalDataSource and tasksRemoteDataSource inside DefaultTasksRepository, they are essentially hard-coded. There is no way to swap in your test double.

What you want to do instead, is provide these data sources to the class, instead of hard-coding them. Providing dependencies is known as dependency injection. There are different ways to provide dependencies, and therefore different types of dependency injection.

Constructor Dependency Injection allows you to swap in the test double by passing it into the constructor.

No injection

Injection

Step 1: Use Constructor Dependency Injection in DefaultTasksRepository

  1. Change the DefaultTasksRepository's constructor from taking in an Application to taking in both data sources and the coroutine dispatcher.

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Because you passed the dependencies in, remove the init method. You no longer need to create the dependencies.
  2. Also delete the old instance variables. You're defining them in the constructor:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Finally, update the getRepository method to use the new constructor:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

You are now using constructor dependency injection!

Step 2: Use your FakeDataSource in your tests

Now that your code is using constructor dependency injection, you can use your fake data source to test your DefaultTasksRepository.

  1. Right-click on the DefaultTasksRepository class name and select Generate, then Test.
  2. Follow the prompts to create DefaultTasksRepositoryTest in the test source set.
  3. At the top of your new DefaultTasksRepositoryTest class, add the member variables below to represent the data in your fake data sources.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Create three variables, two FakeDataSource member variables (one for each data source for your repository) and a variable for the DefaultTasksRepository which you will test.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Make a method to set up and initialize a testable DefaultTasksRepository. This DefaultTasksRepository will use your test double, FakeDataSource.

  1. Create a method called createRepository and annotate it with @Before.
  2. Instantiate your fake data sources, using the remoteTasks and localTasks lists.
  3. Instantiate your tasksRepository, using the two fake data sources you just created and Dispatchers.Unconfined.

The final method should look like the code below.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

Step 3: Write DefaultTasksRepository getTasks() Test

Time to write a DefaultTasksRepository test!

  1. Write a test for the repository's getTasks method. Check that when you call getTasks with true (meaning that it should reload from the remote data source) that it returns data from the remote data source (as opposed to the local data source).

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

You will get an error when you call

getTasks:

bb35045989edbd3f.png

Step 4: Add runBlockingTest

The coroutine error is expected because getTasks is a suspend function and you need to launch a coroutine to call it. For that, you need a coroutine scope. To resolve this error, you're going to need to add some gradle dependencies for handling launching coroutines in your tests.

  1. Add the required dependencies for testing coroutines to the test source set by using testImplementation.

app/build.gradle

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Don't forget to sync!

kotlinx-coroutines-test is the coroutines test library, specifically meant for testing coroutines. To run your tests, use the function runBlockingTest. This is a function provided by the coroutines test library. It takes in a block of code and then runs this block of code in a special coroutine context which runs synchronously and immediately, meaning actions will occur in a deterministic order. This essentially makes your coroutines run like non-coroutines, so it is meant for testing code.

Use runBlockingTest in your test classes when you're calling a suspend function. You'll learn more about how runBlockingTest works and how to test coroutines in the next codelab in this series.

  1. Add the @ExperimentalCoroutinesApi above the class. This expresses that you know you're using an experimental coroutine api (runBlockingTest) in the class. Without it, you'll get a warning.
  2. Back in your DefaultTasksRepositoryTest, add runBlockingTest so that it takes in your entire test as a "block" of code

This final test looks like the code below.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

}
  1. Run your new getTasks_requestsAllTasksFromRemoteDataSource test and confirm it works and the error is gone!

6. Task: Set up a Fake Repository

You've just seen how to unit test a repository. In these next steps, you're going to again use dependency injection and create another test double—this time to show how to write unit and integration tests for your view models.

Unit tests should only test the class or method you're interested in. This is known as testing in isolation, where you clearly isolate your "unit" and only test the code that is part of that unit.

So TasksViewModelTest should only test TasksViewModel code—it should not test code in the database, network, or repository classes. Therefore for your view models, much like you just did for your repository, you'll create a fake repository and apply dependency injection to use it in your tests.

In this task, you apply dependency injection to view models.

2ee5bcac127f3952.png

Step 1. Create a TasksRepository Interface

The first step towards using constructor dependency injection is to create a common interface shared between the fake and the real class.

How does this look in practice? Look at TasksRemoteDataSource, TasksLocalDataSource and FakeDataSource, and notice that they all share the same interface: TasksDataSource. This allows you to say in the constructor of DefaultTasksRepository that you take in a TasksDataSource.

DefaultTasksRepository.kt

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

This is what allows us to swap in your FakeDataSource!

Next, make an interface for DefaultTasksRepository, as you did for the data sources. It needs to include all of the public methods (public API surface) of DefaultTasksRepository.

  1. Open DefaultTasksRepository and right-click on the class name. Then select Refactor -> Extract -> Interface.

638b33aa1d3fe91d.png

  1. Choose Extract to separate file.

76daf401b9f0bb9c.png

  1. In the Extract Interface window, change the interface name to TasksRepository.
  2. In the Members to form interface section, check all members except the two companion members and the private methods.

d97bdbdf2478fe24.png

  1. Click Refactor. The new TasksRepository interface should appear in the data/source package.

558f53d1462cf2d5.png

And DefaultTasksRepository now implements TasksRepository.

  1. Run your app (not the tests) to make sure everything is still in working order.

Step 2. Create FakeTestRepository

Now that you have the interface, you can create the DefaultTasksRepository test double.

  1. In the test source set, in data/source create the Kotlin file and class FakeTestRepository.kt and extend from the TasksRepository interface.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

You will be told you need to implement the interface methods.

  1. Hover over the error until you see the suggestion menu, then click and select Implement members.
  2. Select all of the methods and press OK.

7ce0211e1f7f91bb.png

Step 3. Implement FakeTestRepository methods

You now have a FakeTestRepository class with "not implemented" methods. Similar to how you implemented the FakeDataSource, the FakeTestRepository will be backed by a data structure, instead of dealing with a complicated mediation between local and remote data sources.

Note that your FakeTestRepository doesn't need to use FakeDataSources or anything like that; it just needs to return realistic fake outputs given inputs. You will use a LinkedHashMap to store the list of tasks and a MutableLiveData for your observable tasks.

  1. In FakeTestRepository, add both a LinkedHashMap variable representing the current list of tasks and a MutableLiveData for your observable tasks.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

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

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


    // Rest of class
}

Implement the following methods:

  1. getTasks—This method should take the tasksServiceData and turn it into a list using tasksServiceData.values.toList() and then return that as a Success result.
  2. refreshTasks—Updates the value of observableTasks to be what is returned by getTasks().
  3. observeTasks—Creates a coroutine using runBlocking and run refreshTasks, then returns observableTasks.

For your test double, use runBlocking. runBlocking, which is a closer simulation of what a "real" implementation of the repository would do, and it's preferable for Fakes, so that their behavior more closely matches the real implementation.

When you're in test classes, meaning classes with @Test functions , use runBlockingTest to get deterministic behavior.

Below is the code for those methods.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

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

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

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

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

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

    override suspend fun completeTask(task: Task) {
       val completedTask = task.copy(isCompleted = true)
       tasksServiceData[task.id] = completedTask
       refreshTasks()
     }

    // Rest of class

}

Step 4. Add a method for testing to addTasks

When testing, it is better to have some Tasks already in your repository. You could call saveTask several times, but to make this easier, add a helper method specifically for tests that lets you add tasks.

  1. Add the addTasks method, which takes in a vararg of tasks, adds each to the HashMap, and then refreshes the tasks.

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

At this point you have a fake repository for testing with a few of the key methods implemented. Next, use this in your tests!

7. Task: Use the Fake Repository inside a ViewModel

In this task you use a fake class inside of a ViewModel. Use constructor dependency injection, to take in the two data sources via constructor dependency injection by adding a TasksRepository variable to the TasksViewModel's constructor.

This process is a little different with view models because you don't construct them directly. For example:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}

As in the code above, you're using the viewModel's property delegate which creates the view model. To change how the view model is constructed, you'll need to add and use a ViewModelProvider.Factory. If you're not familiar with ViewModelProvider.Factory, you can learn more about it here.

Step 1. Make and use a ViewModelFactory in TasksViewModel

You start with updating the classes and test related to the Tasks screen.

  1. Open TasksViewModel.
  2. Change the constructor of TasksViewModel to take in TasksRepository instead of constructing it inside the class.

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

Since you changed the constructor, you now need to use a factory to construct TasksViewModel. For convenience you can put the factory class in the same file as TasksViewModel, but you could also put it in its own file.

  1. At the bottom of the TasksViewModel file, outside the class, add a TasksViewModelFactory which takes in a plain TasksRepository.

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}

This is the standard way to change how ViewModels are constructed. Now that you have the factory, use it wherever you construct your view model.

  1. Update TasksFragment to use the factory.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Run your app code and make sure everything is still working!

Step 2. Use FakeTestRepository inside TasksViewModelTest

Now instead of using the real repository in your view model tests, you can use the fake repository.

  1. Open up TasksViewModelTest. It's under the tasks folder in the test source set.
  2. Add a FakeTestRepository property in the TasksViewModelTest.

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. Update the setupViewModel method to make a FakeTestRepository with three tasks, and then construct the tasksViewModel with this repository.

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. Because you are no longer using the AndroidX Test ApplicationProvider.getApplicationContext code, you can also remove the @RunWith(AndroidJUnit4::class) annotation.
  2. Run your tests, make sure they all still work!

By using constructor dependency injection, you've now removed the DefaultTasksRepository as a dependency and replaced it with your FakeTestRepository in the tests.

Step 3. Also update TaskDetailFragment and ViewModel

Make the same changes for the TaskDetailFragment and TaskDetailViewModel. This will prepare the code for when you write TaskDetail tests next.

  1. Open TaskDetailViewModel.
  2. Update the constructor:

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. At the bottom of the TaskDetailViewModel file, outside the class, add a TaskDetailViewModelFactory.

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. Update TaskDetailFragment to use the factory.

TaskDetailFragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Run your code and make sure everything is working.

You are now able to use a FakeTestRepository instead of the real repository in TasksFragment and TaskDetailFragment.

8. Task: Launch a Fragment from a Test

Next you'll write integration tests to test your fragment and view-model interactions. You'll find out if your view model code appropriately updates your UI. To do this you use

  • the ServiceLocator pattern
  • the Espresso and Mockito libraries

Integration tests test the interaction of several classes to make sure they behave as expected when used together. These tests can be run either locally (test source set) or as instrumentation tests (androidTest source set).

7017a2dd290e68aa.png

In your case you'll be taking each fragment and writing integration tests for the fragment and view model to test the main features of the fragment.

Step 1. Add Gradle Dependencies

  1. Add the following gradle dependencies.

app/build.gradle

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

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

These dependencies include:

  • junit:junit—JUnit, which is necessary for writing basic test statements.
  • androidx.test:core—Core AndroidX test library
  • kotlinx-coroutines-test—The coroutines testing library
  • androidx.fragment:fragment-testing—AndroidX test library for creating fragments in tests and changing their state.

Since you'll be using these libraries in your androidTest source set, use androidTestImplementation to add them as dependencies.

Step 2. Make a TaskDetailFragmentTest class

The TaskDetailFragment shows information about a single task.

dae7832a0afea061.png

You'll start by writing a fragment test for the TaskDetailFragment since it has fairly basic functionality compared to the other fragments.

  1. Open taskdetail.TaskDetailFragment.
  2. Generate a test for TaskDetailFragment, as you've done before. Accept the default choices and put it in the androidTest source set (NOT the test source set).

d1f60b80b9a92218.png

  1. Add the following annotations to the TaskDetailFragmentTest class.

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

The purpose of these annotation is:

Step 3. Launch a fragment from a test

In this task, you're going to launch TaskDetailFragment using the AndroidX Testing library. FragmentScenario is a class from AndroidX Test that wraps around a fragment and gives you direct control over the fragment's lifecycle for testing. To write tests for fragments, you create a FragmentScenario for the fragment you're testing (TaskDetailFragment).

  1. Copy this test into TaskDetailFragmentTest.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

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

    }

This code above:

  • Creates a task.
  • Creates a Bundle, which represents the fragment arguments for the task that get passed into the fragment).
  • The launchFragmentInContainer function creates a FragmentScenario, with this bundle and a theme.

This is not a finished test yet, because it's not asserting anything. For now, run the test and observe what happens.

  1. This is an instrumented test, so make sure the emulator or your device is visible.
  2. Run the test.

A few things should happen.

  • First, because this is an instrumented test, the test will run on either your physical device (if connected) or an emulator.
  • It should launch the fragment.
  • Notice how it doesn't navigate through any other fragment or have any menus associated with the activity - it is just the fragment.

Finally, look closely and notice that the fragment says "No data" as it doesn't successfully load up the task data.

d14df7b104bdafe.png

Your test both needs to load up the TaskDetailFragment (which you've done) and assert the data was loaded correctly. Why is there no data? This is because you created a task, but you didn't save it to the repository.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

You have this FakeTestRepository, but you need some way to replace your real repository with your fake one for your fragment. You'll do this next!

9. Task: Make a ServiceLocator

In this task, you'll provide your fake repository to your fragment using a ServiceLocator. This will allow you to write your fragment and view model integration tests.

You can't use constructor dependency injection here, as you did before, when you needed to provide a dependency to the view model or repository. Constructor dependency injection requires that you construct the class. Fragments and activities are examples of classes that you don't construct and generally don't have access to the constructor of.

Since you don't construct the fragment, you can't use constructor dependency injection to swap the repository test double (FakeTestRepository) to the fragment. Instead, use the Service Locator pattern. The Service Locator pattern is an alternative to Dependency Injection. It involves creating a singleton class called the "Service Locator", whose purpose is to provide dependencies, both for the regular and test code. In the regular app code (the main source set), all of these dependencies are the regular app dependencies. For the tests, you modify the Service Locator to provide test double versions of the dependencies.

Not using Service Locator

Using a Service Locator

For this codelab app, do the following:

  1. Create a Service Locator class that is able to construct and store a repository. By default it constructs a "normal" repository.
  2. Refactor your code so that when you need a repository, use the Service Locator.
  3. In your testing class, call a method on the Service Locator which swaps out the "normal" repository with your test double.

Step 1. Create the ServiceLocator

Let's make a ServiceLocator class. It'll live in the main source set with the rest of the app code because it's used by the main application code.

Note: The ServiceLocator is a singleton, so use the Kotlin object keyword for the class.

  1. Create the file ServiceLocator.kt in the top level of the main source set.
  2. Define an object called ServiceLocator.
  3. Create database and repository instance variables and set both to null.
  4. Annotate the repository with @Volatile because it could get used by multiple threads (@Volatile is explained in detail here).

Your code should look as shown below.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

Right now the only thing your ServiceLocator needs to do is know how to return a TasksRepository. It'll return a pre-existing DefaultTasksRepository or make and return a new DefaultTasksRepository, if needed.

Define the following functions:

  1. provideTasksRepository—Either provides an already existing repository or creates a new one. This method should be synchronized on this to avoid, in situations with multiple threads running, ever accidentally creating two repository instances.
  2. createTasksRepository—Code for creating a new repository. Will call createTaskLocalDataSource and create a new TasksRemoteDataSource.
  3. createTaskLocalDataSource—Code for creating a new local data source. Will call createDataBase.
  4. createDataBase—Code for creating a new database.

The completed code is below.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

Step 2. Use ServiceLocator in Application

You're going to make a change to your main application code (not your tests) so that you create the repository in one place, your ServiceLocator.

It's important that you only ever make one instance of the repository class. To ensure this, you'll use the Service locator in the TodoApplication class.

  1. At the top level of your package hierarchy, open TodoApplication and create a val for your repository and assign it a repository that is obtained using ServiceLocator.provideTaskRepository.

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Now that you have created a repository in the application, you can remove the old getRepository method in DefaultTasksRepository.

  1. Open DefaultTasksRepository and delete the companion object.

DefaultTasksRepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

Now everywhere you were using getRepository, use the application's taskRepository instead. This ensures that instead of making the repository directly, you are getting whatever repository the ServiceLocator provided.

  1. Open TaskDetailFragement and find the call to getRepository at the top of the class.
  2. Replace this call with a call that gets the repository from TodoApplication.

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. Do the same for TasksFragment.

TasksFragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. For StatisticsViewModel and AddEditTaskViewModel, update the code that acquires the repository to use the repository from the TodoApplication.
// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Run your application (not the test)!

Since you only refactored, the app should run the same without issue.

Step 3. Create FakeAndroidTestRepository

You already have a FakeTestRepository in the test source set. You cannot share test classes between the test and androidTest source sets by default. So, you need to make a duplicate FakeTestRepository class in the androidTest source set, and call it FakeAndroidTestRepository.

  1. Right-click the androidTest source set and make a data.source package.
  2. Make a new class in this source package called FakeAndroidTestRepository.kt.
  3. Copy the following code to that class.

FakeAndroidTestRepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap


class FakeAndroidTestRepository : TasksRepository {

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

    private var shouldReturnError = false

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

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

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

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

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

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

Step 4. Prepare your ServiceLocator for Tests

Okay, time to use the ServiceLocator to swap in test doubles when testing. To do that, you need to add some code to your ServiceLocator code.

  1. Open ServiceLocator.kt.
  2. Mark the setter for tasksRepository as @VisibleForTesting. This annotation is a way to express that the reason the setter is public is because of testing.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Whether you run your test alone or in a group of tests, your tests should run exactly the same. What this means is that your tests should have no behavior that is dependent on one another (which means avoiding sharing objects between tests).

Since the ServiceLocator is a singleton, it has the possibility of being accidentally shared between tests. To help avoid this, create a method that properly resets the ServiceLocator state between tests.

  1. Add an instance variable called lock with the Any value.

ServiceLocator.kt

private val lock = Any()
  1. Add a testing-specific method called resetRepository which clears out the database and sets both the repository and database to null.

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

Step 5. Use your ServiceLocator

In this step, you use the ServiceLocator.

  1. Open TaskDetailFragmentTest.
  2. Declare a lateinit TasksRepository variable.
  3. Add a setup and a tear down method to set up a FakeAndroidTestRepository before each test and clean it up after each test.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Wrap the function body of activeTaskDetails_DisplayedInUi() in runBlockingTest.
  2. Save activeTask in the repository before launching the fragment.
repository.saveTask(activeTask)

The final test looks like this code below.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

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

    }
  1. Annotate the whole class with @ExperimentalCoroutinesApi.

When finished, the code will look like this.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

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

    }

}
  1. Run the activeTaskDetails_DisplayedInUi() test.

Much like before, you should see the fragment, except this time, because you properly set up the repository, it now shows the task information.

928dc8f5392a5823.png

10. Task: Writing your first Integration Test with Espresso

In this step, you'll use the Espresso UI testing library to complete your first integration test. You have structured your code so you can add tests with assertions for your UI. To do that, you'll use the Espresso testing library.

Espresso helps you:

  • Interact with views, like clicking buttons, sliding a bar, or scrolling down a screen.
  • Assert that certain views are on screen or are in a certain state (such as containing particular text, or that a checkbox is checked, etc.).

Step 1. Note Gradle Dependency

You'll already have the main Espresso dependency since it is included in Android projects by default.

app/build.gradle

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core—This core Espresso dependency is included by default when you make a new Android project. It contains the basic testing code for most views and actions on them.

Step 2. Turn off animations

Espresso tests run on a real device and thus are instrumentation tests by nature. One issue that arises is animations: If an animation lags and you try to test if a view is on screen, but it's still animating, Espresso can accidentally fail a test. This can make Espresso tests flaky.

For Espresso UI testing, it's best practice to turn animations off (also your test will run faster!):

  1. On your testing device, go to Settings > Developer options.
  2. Disable these three settings: Window animation scale, Transition animation scale, and Animator duration scale.

192483c9a6e83a0.png

Step 3. Look at an Espresso test

Before you write an Espresso test, take a look at some Espresso code.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

What this statement does is find the checkbox view with the id task_detail_complete_checkbox, clicks it, then asserts that it is checked.

The majority of Espresso statements are made up of four parts:

  1. Static Espresso method
onView

onView is an example of a static Espresso method that starts an Espresso statement. onView is one of the most common ones, but there are other options, such as onData.

  1. ViewMatcher
withId(R.id.task_detail_title_text)

withId is an example of a ViewMatcher which gets a view by its ID. There are other view matchers which you can look up in the documentation.

  1. ViewAction
perform(click())

The perform method which takes a ViewAction. A ViewAction is something that can be done to the view, for example here, it's clicking the view.

  1. ViewAssertion
check(matches(isChecked()))

check which takes a ViewAssertion. ViewAssertions check or assert something about the view. The most common ViewAssertion you'll use is the matches assertion. To finish the assertion, use another ViewMatcher, in this case isChecked.

e26de7f5db091867.png

Note that you don't always call both perform and check in an Espresso statement. You can have statements that just make an assertion using check or just do a ViewAction using perform.

  1. Open TaskDetailFragmentTest.kt.
  2. Update the activeTaskDetails_DisplayedInUi test.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // 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
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Here are the import statements, if needed:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. Everything after the // THEN comment uses Espresso. Examine the test structure and the use of withId and check to make assertions about how the detail page should look.
  2. Run the test and confirm it passes.

Step 4. Optional, Write your own Espresso Test

Now write a test yourself.

  1. Create a new test called completedTaskDetails_DisplayedInUi and copy this skeleton code.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. Looking at the previous test, complete this test.
  2. Run and confirm the test passes.

The finished completedTaskDetails_DisplayedInUi should look like this code.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.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
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

11. Task: Using Mockito to write Navigation tests

In this last step you'll learn how to test the Navigation component, using a different type of test double called a mock, and the testing library Mockito.

In this codelab you've used a test double called a fake. Fakes are one of many types of test doubles. Which test double should you use for testing the Navigation component?

Think about how navigation happens. Imagine pressing one of the tasks in the TasksFragment to navigate to a task detail screen.

920d31294d1cef2e.png

Here's code in TasksFragment that navigates to a task detail screen when it is pressed.

TasksFragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}

The navigation occurs because of a call to the navigate method. If you needed to write an assert statement, there isn't a straightforward way to test whether you've navigated to TaskDetailFragment. Navigating is a complicated action that doesn't result in a clear output or state change, beyond initializing TaskDetailFragment.

What you can assert is that the navigate method was called with the correct action parameter. This is exactly what a mock test double does—it checks whether specific methods were called.

Mockito is a framework for making test doubles. While the word mock is used in the API and name, it is not for just making mocks. It can also make stubs and spies.

You will be using Mockito to make a mock NavigationController which can assert that the navigate method was called correctly.

Step 1. Add Gradle Dependencies

  1. Add the gradle dependencies.

app/build.gradle

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core—This is the Mockito dependency.
  • dexmaker-mockito—This library is required to use Mockito in an Android project. Mockito needs to generate classes at runtime. On Android, this is done using dex byte code, and so this library enables Mockito to generate objects during runtime on Android.
  • androidx.test.espresso:espresso-contrib—This library is made up of external contributions (hence the name) which contain testing code for more advanced views, such as DatePicker and RecyclerView. It also contains Accessibility checks and class called CountingIdlingResource that is covered later.

Step 2. Create TasksFragmentTest

  1. Open TasksFragment.
  2. Right-click on the TasksFragment class name and select Generate then Test. Create a test in the androidTest source set.
  3. Copy this code to the TasksFragmentTest.

TasksFragmentTest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

This code looks similar to the TaskDetailFragmentTest code you wrote. It sets up and tears down a FakeAndroidTestRepository. Add a navigation test to test that when you click on a task in the task list, it takes you to the correct TaskDetailFragment.

  1. Add the test clickTask_navigateToDetailFragmentOne.

TasksFragmentTest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Use Mockito's mock function to create a mock.

TasksFragmentTest.kt

 val navController = mock(NavController::class.java)

To mock in Mockito, pass in the class you want to mock.

Next, you need to associate your NavController with the fragment. onFragment lets you call methods on the fragment itself.

  1. Make your new mock the fragment's NavController.
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Add the code to click on the item in the RecyclerView that has the text "TITLE1".
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions is part of the espresso-contrib library and lets you perform Espresso actions on a RecyclerView.

  1. Verify that navigate was called, with the correct argument.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Mockito's verify method is what makes this a mock—you're able to confirm the mocked navController called a specific method (navigate) with a parameter (actionTasksFragmentToTaskDetailFragment with the ID of "id1").

The complete test looks like this:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Run your test!

In summary, to test navigation you can:

  1. Use Mockito to create a NavController mock.
  2. Attach that mocked NavController to the fragment.
  3. Verify that navigate was called with the correct action and parameter(s).

Step 3. Optional, write clickAddTaskButton_navigateToAddEditFragment

To see if you can write a navigation test yourself, try this task.

  1. Write the test clickAddTaskButton_navigateToAddEditFragment which checks that if you click on the + FAB, you navigate to the AddEditTaskFragment.

The answer is below.

TasksFragmentTest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

12. Solution code

Click here to see a diff between the code you started and the final code.

To download the code for the finished codelab, you can use the git command below:

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

Alternatively you can download the repository as a Zip file, unzip it, and open it in Android Studio.

13. Summary

This codelab covered how to set up manual dependency injection, a service locator, and how to use fakes and mocks in your Android Kotlin apps. In particular:

  • What you want to test and your testing strategy determine the kinds of test you are going to implement for your app. Unit tests are focused and fast. Integration tests verify interaction between parts of your program. End-to-end tests verify features, have the highest fidelity, are often instrumented, and may take longer to run.
  • The architecture of your app influences how hard it is to test.
  • To isolate parts of your app for testing, you can use test doubles. A test double is a version of a class crafted specifically for testing. For example, you fake getting data from a database or the internet.
  • Use dependency injection to replace a real class with a testing class, for example, a repository or a networking layer.
  • Use instrumented testing (androidTest) to launch UI components.
  • When you can't use constructor dependency injection, for example to launch a fragment, you can often use a service locator. The Service Locator pattern is an alternative to Dependency Injection. It involves creating a singleton class called the "Service Locator", whose purpose is to provide dependencies, both for the regular and test code.

14. Learn more

Samples:

  • Official Testing Sample - This is the official testing sample, which is based off of the same TO-DO Notes app used here. Concepts in this sample go beyond what is covered in the three testing codelabs.
  • Sunflower demo - This is the main Android Jetpack sample which also makes use of the Android testing libraries
  • Espresso testing samples

Udacity course:

Android developer documentation:

Videos:

Other:

15. Next codelab

Start the next lesson: 5.3: Survey of Testing Topics