(Deprecated) Advanced Android in Kotlin 05.1: Testing Basics

1. Welcome

Introduction

When you implemented the first feature of your first app, you likely ran the code to verify that it worked as expected. You performed a test, albeit a manual test. As you continued to add and update features, you probably also continued to run your code and verify it works. But doing this manually every time is tiring, prone to mistakes, and does not scale.

Computers are great at scaling and automation! So developers at companies large and small write automated tests, which are tests that are run by software and do not require you to manually operate the app to verify the code works.

What you'll learn in this series of codelabs is how to create a collection of tests (known as a testing suite) for a real-world app.

This first codelab covers the basics of testing on Android, you'll write your first tests and learn how to test LiveData and ViewModels.

What you should already know

You should be familiar with:

What you'll learn

You'll learn about the following topics:

  • How to write and run unit tests on Android
  • How to use Test Driven Development
  • How to choose instrumented tests and local tests

You'll learn about the following libraries and code concepts:

What you'll do

  • Set up, run, and interpret both local and instrumented tests in Android.
  • Write unit tests in Android using JUnit4 and Hamcrest.
  • Write simple LiveData and ViewModel 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.

e490df637e1bf10c.gif

This app is written in Kotlin, has several 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.

3. Getting Started

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 starter_code

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

4. Task: Familiarizing yourself with the code

In this task you'll run the app and explore the code base.

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.

5. Task: Running tests

In this task, you'll run your first tests.

  1. In Android Studio, open up the Project pane and find these three folders:
  • com.example.android.architecture.blueprints.todoapp
  • com.example.android.architecture.blueprints.todoapp (androidTest)
  • com.example.android.architecture.blueprints.todoapp (test)

These folders are known as source sets. Source sets are folders containing source code for your app. The source sets, which are colored green (androidTest and test) contain your tests. When you create a new Android project, you get the following three source sets by default. They are:

  • main: Contains your app code. This code is shared amongst all different versions of the app you can build (known as build variants)
  • androidTest: Contains tests known as instrumented tests.
  • test: Contains tests known as local tests.

The difference between local tests and instrumented tests is in the way they are run.

Local tests (test source set)

These tests are run locally on your development machine's JVM and do not require an emulator or physical device. Because of this, they run fast, but their fidelity is lower, meaning they act less like they would in the real world.

In Android Studio local tests are represented by a green and red triangle icon.

9060ac11ceb5e66e.png

Instrumented tests (androidTest source set)

These tests run on real or emulated Android devices, so they reflect what will happen in the real world, but are also much slower.

In Android Studio instrumented tests are represented by an Android with a green and red triangle icon.

6df04e9088327cf8.png

Step 1: Run a local test

  1. Open the test folder until you find the ExampleUnitTest.kt file.
  2. Right-click on it and select Run ExampleUnitTest.

You should see the following output in the Run window at the bottom of the screen:

cb237d020d5ed709.png

  1. Notice the green checkmarks and expand the test results to confirm that one test called addition_isCorrect passed. It's good to know that addition works as expected!

Step 2: Make the test fail

Below is the test that you just ran.

ExampleUnitTest.kt

// A test class is just a normal class
class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       // Here you are checking that 4 is the same as 2+2
       assertEquals(4, 2 + 2)
   }
}

Notice that tests

  • are a class in one of the test source sets.
  • contain functions that start with the @Test annotation (each function is a single test).
  • usually contain assertion statements.

Android uses the testing library JUnit for testing (in this codelab JUnit4). Both assertions and the @Test annotation come from JUnit.

An assertion is the core of your test. It's a code statement that checks that your code or app behaved as expected. In this case, the assertion is assertEquals(4, 2 + 2) which checks that 4 is equal to 2 + 2.

To see what a failed test looks like add an assertion that you can easily see should fail. It'll check that 3 equals 1+1.

  1. Add assertEquals(3, 1 + 1) to the addition_isCorrect test.

ExampleUnitTest.kt

class ExampleUnitTest {

   // Each test is annotated with @Test (this is a Junit annotation)
   @Test
   fun addition_isCorrect() {
       assertEquals(4, 2 + 2)
       assertEquals(3, 1 + 1) // This should fail
   }
}
  1. Run the test.
  2. In the test results, notice an X next to the test.

e80a71def694097f.png

  1. Also notice:
  • A single failed assertion fails the entire test.
  • You are told the expected value (3) versus the value that was actually calculated (2).
  • You are directed to the line of the failed assertion (ExampleUnitTest.kt:16).

Step 3: Run an instrumented test

Instrumented tests are in the androidTest source set.

  1. Open the androidTest source set.
  2. Run the test called ExampleInstrumentedTest.

ExampleInstrumentedTest

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun useAppContext() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("com.example.android.architecture.blueprints.reactive",
            appContext.packageName)
    }
}

Unlike the local test, this test runs on a device (in the example below an emulated Pixel 2 phone):

cbc15c3229c7deec.png

If you have a device attached or an emulator running, you should see the test run on the emulator.

6. Task: Writing your first test

In this task, you'll write tests for getActiveAndCompleteStats, which calculates the percentage of active and complete task stats for your app. You can see these numbers on the statistics screen of the app.

7abfbf08efb1b623.png

Step 1: Create a test class

  1. In the main source set, in todoapp.statistics, open StatisticsUtils.kt.
  2. Find the getActiveAndCompletedStats function.

StatisticsUtils.kt

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)

The getActiveAndCompletedStats function accepts a list of tasks and returns a StatsResult. StatsResult is a data class that contains two numbers, the percentage of tasks that are completed, and the percentage that are active.

Android Studio gives you tools to generate test stubs to help you implement the tests for this function.

  1. Right click getActiveAndCompletedStats and select Generate > Test.

The Create Test dialog opens:

1eb4d2bcea2a5323.png

  1. Change the Class name: to StatisticsUtilsTest (instead of StatisticsUtilsKtTest; it's slightly nicer not to have KT in the test class name).
  2. Keep the rest of the defaults. JUnit 4 is the appropriate testing library. The destination package is correct (it mirrors the location of the StatisticsUtils class) and you don't need to check any of the check boxes (this just generates extra code, but you'll write your test from scratch).
  3. Press OK.

The Choose Destination Directory dialog opens: 3342fe4d590f2129.png

You'll be making a local test because your function is doing math calculations and won't include any Android specific code. So, there's no need to run it on a real or emulated device.

  1. Select the test directory (not androidTest) because you'll be writing local tests.
  2. Click OK.
  3. Notice the generated the StatisticsUtilsTest class in test/statistics/.

2fcd839638adcdfc.png

Step 2: Write your first test function

You're going to write a test that checks:

  • if there are no completed tasks and one active task,
  • that the percentage of active tests is 100%,
  • and the percentage of completed tasks is 0%.
  1. Open StatisticsUtilsTest.
  2. Create a function named getActiveAndCompletedStats_noCompleted_returnsHundredZero.

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. Add the @Test annotation above the function name to indicate it's a test.
  2. Create a list of tasks.
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. Call getActiveAndCompletedStats with these tasks.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. Check that result is what you expected, using assertions.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

Here is the complete code.

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active task (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertEquals(result.completedTasksPercent, 0f)
        assertEquals(result.activeTasksPercent, 100f)
    }
}
  1. Run the test (Right click StatisticsUtilsTest and select Run).

It should pass:

2d4423767733aac8.png

Step 3: Add the Hamcrest dependency

Because your tests act as documentation of what your code does, it's nice when they are human readable. Compare the following two assertions:

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

The second assertion reads much more like a human sentence. It is written using an assertion framework called Hamcrest. Another good tool for writing readable assertions is the Truth library. You'll be using Hamcrest in this codelab to write assertions.

  1. Open build.grade (Module: app) and add the following dependency.

app/build.gradle

dependencies {
    // Other dependencies
    testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}

Usually, you use implementation when adding a dependency, yet here you're using testImplementation. When you're ready to share your app with the world, it is best not to bloat the size of your APK with any of the test code or dependencies in your app. You can designate whether a library should be included in the main or test code by using gradle configurations. The most common configurations are:

  • implementation—The dependency is available in all source sets, including the test source sets.
  • testImplementation—The dependency is only available in the test source set.
  • androidTestImplementation—The dependency is only available in the androidTest source set.

Which configuration you use, defines where the dependency can be used. If you write:

testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"

This means that Hamcrest will only be available in the test source set. It also ensures that Hamcrest will not be included in your final app.

Step 4: Use Hamcrest to write assertions

  1. Update the getActiveAndCompletedStats_noCompleted_returnsHundredZero() test to use Hamcrest's assertThat instead of assertEquals.
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))

Note you can use the import import org.hamcrest.Matchers.is`` if prompted.

The final test will look like the code below.

StatisticsUtilsTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {

        // Create an active tasks (the false makes this active)
        val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
        // Call your function
        val result = getActiveAndCompletedStats(tasks)

        // Check the result
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))

    }
}
  1. Run your updated test to confirm it still works!

This codelab will not teach you all the ins and outs of Hamcrest, so if you'd like to learn more check out the official tutorial.

subjectUnderTest_actionOrInput_resultState

  • Subject under test is the method or class that is being tested (getActiveAndCompletedStats).
  • Next is the action or input (noCompleted).
  • Finally you have the expected result (returnsHundredZero).

7. Task: Writing more tests

This is an optional task for practice.

In this task, you'll write more tests using JUnit and Hamcrest. You'll also write tests using a strategy derived from the program practice of Test Driven Development. Test Driven Development or TDD is a school of programming thought that says instead of writing your feature code first, you write your tests first. Then you write your feature code with the goal of passing your tests.

Step 1. Write the tests

Write tests for when you have a normal task list:

  1. If there is one completed task and no active tasks, the activeTasks percentage should be 0f, and the completed tasks percentage should be 100f .
  2. If there are two completed tasks and three active tasks, the completed percentage should be 40f and the active percentage should be 60f.

Step 2. Write a test for a bug

The code for the getActiveAndCompletedStats as written has a bug. Notice how it does not properly handle what happens if the list is empty or null. In both of these cases, both percentages should be zero.

internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

   val totalTasks = tasks!!.size
   val numberOfActiveTasks = tasks.count { it.isActive }
   val activePercent = 100 * numberOfActiveTasks / totalTasks
   val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks

   return StatsResult(
       activeTasksPercent = activePercent.toFloat(),
       completedTasksPercent = completePercent.toFloat()
   )
  
}

To fix the code and write tests, you'll use test driven development. Test Driven Development follows these steps.

  1. Write the test, using the Given, When, Then structure, and with a name that follows the convention.
  2. Confirm the test fails.
  3. Write the minimal code to get the test to pass.
  4. Repeat for all tests!

5408e4b11ef2d3eb.png

Instead of starting by fixing the bug, you'll start by writing the tests first. Then you can confirm that you have tests protecting you from ever accidentally reintroducing these bugs in the future.

  1. If there is an empty list (