测试基础知识

此 Codelab 是“使用 Kotlin 进行高级 Android 开发”课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘课程的价值,但并不强制要求这样做。“使用 Kotlin 进行高级 Android 开发”Codelab 着陆页列出了所有课程 Codelab。

简介

实现第一个应用的第一个功能时,您可能运行了代码以验证它是否按预期运行。您进行了测试,虽然是手动测试。随着您不断添加和更新功能,您可能仍会继续运行代码并验证其是否正常运行。但每次都手动执行此操作会很疲劳,容易出错,而且不能扩大规模。

计算机擅长扩缩和自动化!因此,不同规模的公司编写的自动化测试是由软件运行的测试,不需要您手动运行应用来验证代码是否正常工作。

在这一系列 Codelab 中,您将学会如何为实际应用创建一系列测试(称为“测试套件”)。

此 Codelab 涵盖了 Android 测试基础知识,您将编写您的第一项测试,并了解如何测试 LiveDataViewModel

您应当已掌握的内容

您应熟悉以下内容/操作:

学习内容

您将了解到以下主题:

  • 如何在 Android 上编写和运行单元测试
  • 如何使用 Test Drived Development
  • 如何选择插桩测试和本地测试

您将了解以下库和代码概念:

您将执行的操作

  • 在 Android 中设置、运行和解读本地测试和插桩测试。
  • 使用 JUnit4 和 Hamcrest 在 Android 中编写单元测试。
  • 编写简单的 LiveDataViewModel 测试。

在这一系列 Codelab 中,您将使用 TO-DO Notes 应用。该应用允许您编写任务以完成任务,并以列表形式显示这些任务。然后,您可以将这些任务标记为“已完成”、过滤或将其删除。

此应用使用 Kotlin 编写,具有多种屏幕,使用 Jetpack 组件,并遵循应用架构指南中的架构。通过学习如何测试此应用,您将能够测试使用相同库和架构的应用。

首先,请下载代码:

下载 Zip 文件

或者,您也可以克隆代码的 GitHub 代码库:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout starter_code

在此任务中,您将运行应用并探索代码库。

第 1 步:运行示例应用

下载 TO-DO 应用后,在 Android Studio 中打开它并运行它。它应该编译。执行以下操作,浏览该应用:

  • 使用悬浮操作按钮创建新任务。先输入标题,然后输入任务的其他信息。使用绿色对勾 FAB 保存。
  • 在任务列表中,点击您刚刚完成的任务的标题,并查看该任务的详细信息屏幕,以查看其余说明。
  • 在列表或详情屏幕中,选中该任务的复选框,将其状态设为已完成
  • 返回到任务屏幕,打开过滤条件菜单,按有效已完成状态过滤任务。
  • 打开抽屉式导航栏,然后点击统计信息
  • 返回概览屏幕,然后在抽屉式导航栏菜单中选择清除已完成的任务,从而删除已完成状态的所有任务。

第 2 步:探索示例应用代码

“待完成”应用基于热门的架构蓝图测试和架构示例(使用示例的响应式架构版本)。应用遵循应用架构指南中的架构。它将 ViewModel 与 Fragment、存储库和 Room 一起使用。如果您熟悉以下任一示例,这款应用都有类似的架构:

更重要的是了解应用的一般架构,而不是深入了解任一层的逻辑。

以下是软件包摘要:

软件包com.example.android.architecture.blueprints.todoapp

.addedittask

添加或修改任务屏幕:用于添加或修改任务的界面层代码。

.data

数据层:处理任务的数据层。它包含数据库、网络和代码库代码。

.statistics

统计信息屏幕:统计信息屏幕的界面层代码。

.taskdetail

任务详情屏幕:单个任务的界面层代码。

.tasks

任务屏幕:所有任务的列表的界面层代码。

.util

实用程序类:应用的各个部分共用的类,例如用于多个屏幕的滑动刷新布局。

数据层 (.data)

该应用包含模拟网络层(位于 remote 软件包中)和数据库层(位于 local 软件包中)。为简单起见,在此项目中,网络层仅通过 HashMap 进行模拟,而非实际发出网络请求。

DefaultTasksRepository 会在网络层与数据库层之间协调或协调,数据会返回到界面层。

界面层(.addedittask、.statistics、.taskdetail、.tasks)

每个界面层软件包都包含一个 fragment 和视图模型,以及界面所需的任何其他类(例如任务列表的适配器)。TaskActivity 是包含所有 fragment 的 activity。

导航

应用的导航由 Navigation 组件控制。它在 nav_graph.xml 文件中定义。使用 Event 类在视图模型中触发导航;视图模型也会确定要传递的参数。fragment 会观察 Event 并在屏幕之间执行实际导航。

在此任务中,您将运行第一个测试。

  1. 在 Android Studio 中,打开 Project 窗格并找到以下三个文件夹:
  • com.example.android.architecture.blueprints.todoapp
  • com.example.android.architecture.blueprints.todoapp (androidTest)
  • com.example.android.architecture.blueprints.todoapp (test)

这些文件夹称为源集。源代码集是包含应用源代码的文件夹。绿色集的源代码集(androidTesttest)包含您的测试。创建新的 Android 项目时,默认情况下,您会获得以下三个源代码集。它们是:

  • main:包含应用代码。此代码可以在您可以构建的所有不同应用版本之间共享(称为构建变体
  • androidTest:包含称为插桩测试的测试。
  • test:包含称为本地测试的测试。

本地测试插桩测试的区别在于运行方式不同。

本地测试(test 个源代码集)

这些测试在开发机器的 JVM 本地运行,不需要模拟器或实体设备。因此,它们的运行速度很快,但保真度较低,这意味着它们的行为不像在现实世界中那样。

在 Android Studio 中,本地测试由绿色和红色三角形图标表示。

插桩测试(androidTest 源代码集)

这些测试在真实或模拟的 Android 设备上运行,因此可以反映现实世界中发生的情况,但速度也慢得多。

在 Android Studio 中,插桩测试由具有绿色和红色三角形图标的 Android 表示。

第 1 步:运行本地测试

  1. 打开 test 文件夹,直到找到 ExampleUnitTest.kt 文件。
  2. 右键点击并选择 Run ExampleUnitTest

您应该会在屏幕底部的 Run 窗口中看到以下输出:

  1. 请注意绿色对勾标记并展开测试结果,确认一项名为 addition_isCorrect 的测试已通过。不可不知的小知识:添加功能按预期运行!

第 2 步:让测试失败

以下是您刚刚运行的测试。

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)
   }
}

请注意,测试

  • 是某个测试源代码集中的类。
  • 包含以 @Test 注解开头的函数(每个函数都是一个测试)。
  • 通常包含断言语句。

Android 使用测试库 JUnit 进行测试(在本 Codelab JUnit4 中)。断言和 @Test 注解都来自 JUnit。

断言是测试的核心。它是一个代码语句,用于检查您的代码或应用是否按预期运行。在本例中,断言为 assertEquals(4, 2 + 2),它会检查 4 是否等于 2 + 2。

如需查看失败的测试是什么样子,添加一个很容易看到的断言。并检查 3 是否等于 1+1。

  1. assertEquals(3, 1 + 1) 添加到 addition_isCorrect 测试。

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. 运行测试。
  1. 在测试结果中,请注意测试旁边的 X。

  1. 另请注意:
  • 单个失败的断言会使整个测试失败。
  • 您会得到预期值 (3) 与实际计算的值 (2)。
  • 系统会将您导向失败的断言行 (ExampleUnitTest.kt:16)

第 3 步:运行插桩测试

插桩测试位于 androidTest 源代码集中。

  1. 打开 androidTest 源代码集。
  2. 运行名为 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)
    }
}

与本地测试不同,此测试在设备上运行(在以下示例的 Pixel 2 手机上):

如果您连接了设备或运行模拟器,应该会在模拟器上看到该测试运行。

在此任务中,您将针对 getActiveAndCompleteStats 编写测试,该测试会计算应用的有效和完整任务统计信息所占的百分比。您可以在应用的统计信息屏幕上看到这些数据。

第 1 步:创建测试类

  1. main 源代码集的 todoapp.statistics 中,打开 StatisticsUtils.kt
  2. 找到 getActiveAndCompletedStats 函数。

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)

getActiveAndCompletedStats 函数接受任务列表并返回 StatsResultStatsResult 是一个数据类,其中包含两个数字、“已完成”任务的百分比和“活动”百分比。

Android Studio 为您提供了生成测试存根的工具,帮助您实现此函数的测试。

  1. 右键点击 getActiveAndCompletedStats,然后选择 Generate > Test

此时会打开 Create Test 对话框:

  1. Class name: 更改为 StatisticsUtilsTest(而不是 StatisticsUtilsKtTest;不用在测试类名称中添加 KT)。
  2. 保留其他默认设置。JUnit 4 是合适的测试库。目标软件包正确无误(镜像 StatisticsUtils 类的位置),您无需选中任何复选框(这会生成额外的代码,但您将从头开始编写测试)。
  3. 确定

系统会打开 Choose Destination Directory 对话框:

您将进行本地测试,因为您的函数正在执行数学计算,不会包含任何 Android 专用代码。因此,无需在真实或模拟设备上运行测试。

  1. 选择 test 目录(而非 androidTest),因为您将编写本地测试。
  2. 点击 OK
  3. 请注意在 test/statistics/ 中生成的 StatisticsUtilsTest 类。

第 2 步:编写您的第一个测试函数

您将编写一个检查以下各项的测试:

  • 如果没有任何已完成的任务和一个有效任务,
  • 活跃测试百分比为 100%;
  • 已完成任务的百分比为 0%。
  1. 打开 StatisticsUtilsTest
  2. 创建一个名为 getActiveAndCompletedStats_noCompleted_returnsHundredZero 的函数。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. 在函数名称上方添加 @Test 注解,表示这是一个测试。
  2. 创建任务列表。
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. 使用以下任务调用 getActiveAndCompletedStats
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. 使用断言检查 result 是否符合您的预期。
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

以下是完整代码。

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. 运行测试(右键点击 StatisticsUtilsTest,然后选择 Run)。

它应该传递:

第 3 步:添加 Hamcrest 依赖项

由于测试是代码作用文档,因此如果代码易于理解,那就更好了。比较以下两个断言:

assertEquals(result.completedTasksPercent, 0f)

// versus

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

第二个断言更像是人类句子。它使用一个名为 Hamcrest 的断言框架编写。Truth 库是编写可读断言的理想工具。在此 Codelab 中,您将使用 Hamcrest 编写断言。

  1. 打开 build.grade (Module: app) 并添加以下依赖项。

app/build.gradle

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

通常情况下,您在添加依赖项时使用 implementation,但此处使用的是 testImplementation。当您准备好与全世界共享您的应用时,最好不要让应用中的任何测试代码或依赖项变得过大。您可以使用 gradle 配置来指定是应该在主代码还是测试代码中包含库。最常见的配置包括:

  • implementation - 依赖项在所有源代码集中均可用,包括测试源代码集。
  • testImplementation - 依赖项仅在测试源代码集中可用。
  • androidTestImplementation - 依赖项仅在 androidTest 源代码集中可用。

使用哪种配置定义了依赖项的使用位置。如果您写道:

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

这意味着 Hamcrest 只能在测试源代码集内使用。这还可以确保 Hamcrest 不会包含在最终应用中。

第 4 步:使用 Hamcrest 编写断言

  1. 更新 getActiveAndCompletedStats_noCompleted_returnsHundredZero() 测试,以使用 Hamcrest 的 assertThat,而不是 assertEquals
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)

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

请注意,如果系统提示,您可以使用导入 import org.hamcrest.Matchers.`is`

最终测试将如以下代码所示。

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. 运行更新后的测试,确认它是否仍可正常运行。

此 Codelab 不会为您介绍 Hamcrest 的所有功能,因此,如果您想了解详情,请查看官方教程

这是一个可选练习任务。

在此任务中,您将使用 JUnit 和 Hamcrest 编写更多测试。此外,您还要根据从测试驱动型开发这一计划做法中派生的策略编写测试。测试驱动型开发 (TDD) 是一门编程思想,它表示您先编写测试,而不是先编写功能代码。然后,您将编写功能代码,以便通过测试。

第 1 步:编写测试

编写测试以使用常规任务列表:

  1. 如果有一项已完成的任务,但没有一项处于活跃状态的任务,那么 activeTasks 百分比应为 0f,而已完成任务的百分比应为 100f
  2. 如果有 2 项已完成的任务和 3 项有效的任务,已完成的百分比应为 40f,处于活动状态的百分比应为 60f

第 2 步:编写 bug 测试

写入的 getActiveAndCompletedStats 代码存在错误。请注意,如果列表为空或 null,它无法正确处理情况。在这两种情况下,这两个百分比都应为零。

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()
   )
  
}

如需修复代码并编写测试,您需要使用测试驱动型开发。测试驱动型开发遵循以下步骤。

  1. 使用给定、时间、然后结构,且遵循惯例名称编写测试。
  2. 确认测试失败。
  3. 只需编写极少的代码,即可让测试通过。
  4. 对所有测试重复上述操作!

与其从修正错误开始,不如先编写测试。然后,您可以确认是否有测试可以防止您日后不小心重新引入这些错误。

  1. 如果有一个空列表 (emptyList()),这两个百分比都应为 0f。
  2. 如果加载任务时出错,列表将为 null,两个百分比都应为 0f。
  3. 运行测试并确认测试失败

第 3 步:修复错误

现在您已获得了测试,接下来将修复错误。

  1. 如果 tasksnull 或为空,则返回 0f 来修复 getActiveAndCompletedStats 中的错误:
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}
  1. 再次运行测试,并确认所有测试现在都顺利通过!

通过遵循 TDD 并先编写测试,您帮助确保了:

  • 新功能始终有关联的测试;因此,您的测试将作为测试代码文档。
  • 测试会检查结果是否正确,并防范您已经发现的错误。

解决方案:编写更多测试

以下是所有测试以及相应的功能代码。

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    @Test
    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
        val tasks = listOf(
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed with an active task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 100 and 0
        assertThat(result.activeTasksPercent, `is`(100f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
        val tasks = listOf(
            Task("title", "desc", isCompleted = true)
        )
        // When the list of tasks is computed with a completed task
        val result = getActiveAndCompletedStats(tasks)

        // Then the percentages are 0 and 100
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(100f))
    }

    @Test
    fun getActiveAndCompletedStats_both_returnsFortySixty() {
        // Given 3 completed tasks and 2 active tasks
        val tasks = listOf(
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = true),
            Task("title", "desc", isCompleted = false),
            Task("title", "desc", isCompleted = false)
        )
        // When the list of tasks is computed
        val result = getActiveAndCompletedStats(tasks)

        // Then the result is 40-60
        assertThat(result.activeTasksPercent, `is`(40f))
        assertThat(result.completedTasksPercent, `is`(60f))
    }

    @Test
    fun getActiveAndCompletedStats_error_returnsZeros() {
        // When there's an error loading stats
        val result = getActiveAndCompletedStats(null)

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }

    @Test
    fun getActiveAndCompletedStats_empty_returnsZeros() {
        // When there are no tasks
        val result = getActiveAndCompletedStats(emptyList())

        // Both active and completed tasks are 0
        assertThat(result.activeTasksPercent, `is`(0f))
        assertThat(result.completedTasksPercent, `is`(0f))
    }
}

StatisticsUtils.kt

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

    return if (tasks == null || tasks.isEmpty()) {
        StatsResult(0f, 0f)
    } else {
        val totalTasks = tasks.size
        val numberOfActiveTasks = tasks.count { it.isActive }
        StatsResult(
            activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
            completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
        )
    }
}

太棒了,您已掌握了编写和运行测试的基础知识!接下来,您将学习如何编写基本的 ViewModelLiveData 测试。

在此 Codelab 的其余部分,您将学习如何为大多数应用(ViewModelLiveData)中常见的两种 Android 类编写测试。

首先,为 TasksViewModel 编写测试。


您将专注于在视图模型中采用所有逻辑且不依赖于存储库代码的测试。存储库代码涉及异步代码、数据库和网络调用,这都会增加测试的复杂性。您目前应该避免这种情况,并专注于为 ViewModel 功能编写测试,这些测试不会直接测试代码库中的任何内容。



您编写的测试会检查在调用 addNewTask 方法时,会触发用于打开新任务窗口的 Event。这是您要测试的应用代码。

TasksViewModel.kt

fun addNewTask() {
   _newTaskEvent.value = Event(Unit)
}

第 1 步:创建 TasksViewModelTest 类

按照与 StatisticsUtilTest 相同的步骤,在此步骤中,您将为 TasksViewModelTest 创建一个测试文件。

  1. tasks 软件包 TasksViewModel. 中打开您要测试的类。
  2. 在代码中,右键点击类名称 TasksViewModel -> Generate -> Test

  1. 创建测试屏幕上,点击确定以接受(无需更改任何默认设置)。
  2. Choose Destination Directory 对话框中,选择 test 目录。

第 2 步:开始编写 ViewModel 测试

在此步骤中,您将添加视图模型测试,以便测试当您调用 addNewTask 方法时,是否会触发用于打开新任务窗口的 Event

  1. 创建一个名为 addNewTask_setsNewTaskEvent 的新测试。

TasksViewModelTest.kt

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

应用环境如何?

当您创建要测试的 TasksViewModel 实例时,其构造函数需要一个应用上下文。但是,在此测试中,您并未创建包含 Activity 和界面以及 Fragment 的完整应用,那么如何获取应用上下文呢?

TasksViewModelTest.kt

// Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(???)

AndroidX Test 库包含一些类和方法,可为您提供用于测试的应用和 Activity 等组件版本。当您进行需要模拟 Android 框架类(例如应用上下文)的本地测试时,请按以下步骤操作,以正确设置 AndroidX Test:

  1. 添加 AndroidX Test 核心和扩展依赖项
  2. 添加 Robolectric Testing 库依赖项
  3. 为该类添加 AndroidJunit4 测试运行程序注解
  4. 编写 AndroidX 测试代码

您需要完成这些步骤,然后了解它们的共同作用。

第 3 步:添加 Gradle 依赖项

  1. 将这些依赖项复制到您的应用模块的 build.gradle 文件中,以添加核心 AndroidX Test 核心和 ext 依赖项以及 Robolectric 测试依赖项。

app/build.gradle

    // AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"

    testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"

 testImplementation "org.robolectric:robolectric:$robolectricVersion"

第 4 步:添加 JUnit 测试运行程序

  1. 在测试类上方添加 @RunWith(AndroidJUnit4::class)

TasksViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

第 5 步:使用 AndroidX Test

此时,您可以使用 AndroidX Test 库。这包括获取应用上下文的 ApplicationProvider.getApplicationContext 方法。

  1. 使用 AndroidX 测试库中的 ApplicationProvider.getApplicationContext() 创建 TasksViewModel

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. 致电 tasksViewModel 联系addNewTask

TasksViewModelTest.kt

tasksViewModel.addNewTask()

此时,您的测试应如以下代码所示。

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
  1. 运行测试以确认其有效。

概念:AndroidX Test 的工作原理是什么?

什么是 AndroidX Test?

AndroidX Test 是一系列用于测试的库。它包含的类和方法可为您提供用于测试的组件(例如“应用和 Activity”)的版本。例如,您编写的此代码是一个用于获取应用上下文的 AndroidX Test 函数示例。

ApplicationProvider.getApplicationContext()

AndroidX Test API 的优势之一是适用于本地测试和插桩测试。这很棒,因为:

  • 您可以运行与本地测试或插桩测试相同的测试。
  • 您无需学习用于本地测试和插桩测试的不同测试 API。

例如,由于您是使用 AndroidX Test 库编写代码的,因此您可以将 TasksViewModelTest 类从 test 文件夹移至 androidTest 文件夹,这些测试仍会运行。getApplicationContext() 的工作方式略有不同,具体取决于是作为本地测试还是插桩测试来运行:

  • 如果是插桩测试,它会在启动模拟器或连接到实际设备时获取提供的实际应用上下文。
  • 如果是本地测试,则使用模拟 Android 环境。

什么是 Robolectric?

AndroidX Test 用于进行本地测试的模拟 Android 环境由 Robolectric 提供。Robolectric 是一个库,用于创建用于测试的模拟 Android 环境,比启动模拟器或在设备上运行更快。如果没有 Robolectric 依赖项,您会收到以下错误:

@RunWith(AndroidJUnit4::class)有什么用途?

测试运行程序 是一种 JUnit 组件,用于运行测试。如果没有测试运行程序,您的测试将无法运行。JUnit 提供默认测试运行程序,您可以自动运行该程序。@RunWith 会取代该默认测试运行程序。

AndroidJUnit4 测试运行程序允许 AndroidX Test 以不同方式运行测试,具体取决于是插桩测试还是本地测试。

第 6 步:解决 Robolectric 警告

运行代码时,请注意使用了 Robolectric。

得益于 AndroidX Test 和 AndroidJunit4 测试运行程序,您无需直接编写任何 Robolectric 代码,即可做到这一点!

您可能会注意到两条警告。

  • No such manifest file: ./AndroidManifest.xml
  • "WARN: Android SDK 29 requires Java 9..."

您可以通过更新 Gradle 文件来修复 No such manifest file: ./AndroidManifest.xml 警告。

  1. 将以下行添加到您的 gradle 文件,以便使用正确的 Android 清单。通过 includeAndroidResources 选项,您可以在单元测试中访问 Android 资源,包括 AndroidManifest 文件。

app/build.gradle

    // Always show the result of every unit test when running via command line, even if it passes.
    testOptions.unitTests {
        includeAndroidResources = true

        // ... 
    }

警告 "WARN: Android SDK 29 requires Java 9..." 更加复杂。在 Android Q 上运行测试需要 Java 9。对于此 Codelab,请不要将 Android Studio 配置为使用 Java 9,而应将您的目标和 SDK 编译为 28。

总结

  • 纯视图模型测试通常包含在 test 源代码集中,因为其代码通常不需要 Android。
  • 您可以使用 AndroidX 测试获取组件(例如“应用和 Activity”)的测试版本。
  • 如果需要在 test 源代码集中运行模拟 Android 代码,您可以添加 Robolectric 依赖项和 @RunWith(AndroidJUnit4::class) 注解。

恭喜!您将使用 AndroidX 测试库和 Robolectric 运行测试。您的测试尚未完成(您尚未编写断言语句,它只说 // TODO test LiveData)。接下来,您将学习如何使用 LiveData 编写断言语句。

在此任务中,您将了解如何正确断言 LiveData 值。

这是指您未进行 addNewTask_setsNewTaskEvent 视图模型测试时就退出的位置。

TasksViewModelTest.kt

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        // TODO test LiveData
    }
    

如需测试 LiveData,建议您执行以下两项操作:

  1. 使用“InstantTaskExecutorRule
  2. 确保LiveData观察

第 1 步:使用 InstantTaskExecutorRule

InstantTaskExecutorRuleJUnit 规则。将其与 @get:Rule 注解一起使用时,会导致 InstantTaskExecutorRule 类中的某些代码在测试前后运行(要查看确切的代码,您可以使用键盘快捷键 Command+B 查看文件)。

此规则在同一线程中运行与架构组件相关的所有后台作业,以使测试结果以可重复的顺序同步发生。编写包含 LiveData 测试的测试时,请使用此规则!

  1. 架构组件核心测试库(其中包含此规则)添加 gradle 依赖项。

app/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. 打开 TasksViewModelTest.kt
  2. TasksViewModelTest 类中添加 InstantTaskExecutorRule

TasksViewModelTest.kt

class TasksViewModelTest {
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()
    
    // Other code...
}

第 2 步:添加 LiveDataTestUtil.kt 类

下一步是确保您观察到正在测试的 LiveData

使用 LiveData 时,您通常有一个 activity 或 fragment (LifecycleOwner) 会观察 LiveData

viewModel.resultLiveData.observe(fragment, Observer {
    // Observer code here
})

此观察结果非常重要。您需要在 LiveData 上启用观察器

  • 触发任何 onChanged 事件。
  • 触发任何转换

如需获得视图模型的 LiveData 的预期 LiveData 行为,您需要使用 LifecycleOwner 观察 LiveData

这会带来一个问题:在 TasksViewModel 测试中,您没有 activity 或 fragment 来观察 LiveData。为解决此问题,您可以使用 observeForever 方法,该方法会持续观察 LiveData,而无需 LifecycleOwner。调用 observeForever 时,您需要记得移除观察者或存在观察者泄露的风险。

具体代码如下所示。对其进行检查:

@Test
fun addNewTask_setsNewTaskEvent() {

    // Given a fresh ViewModel
    val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())


    // Create observer - no need for it to do anything!
    val observer = Observer<Event<Unit>> {}
    try {

        // Observe the LiveData forever
        tasksViewModel.newTaskEvent.observeForever(observer)

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.value
        assertThat(value?.getContentIfNotHandled(), (not(nullValue())))

    } finally {
        // Whatever happens, don't forget to remove the observer!
        tasksViewModel.newTaskEvent.removeObserver(observer)
    }
}

这是大量用来在测试中观察单个 LiveData 的样板代码!有几种方法可以消除样板。您将使用名为 LiveDataTestUtil 的扩展函数来简化观察者添加操作。

  1. test 源代码集中创建一个名为 LiveDataTestUtil.kt 的新 Kotlin 文件。


  1. 复制并粘贴以下代码。

LiveDataTestUtil.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException


@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

这是一种相当复杂的方法。它会创建一个名为 getOrAwaitValueKotlin 扩展函数,该函数会添加观察者、获取 LiveData 值并清理观察者 - 基本上是上面所示的 observeForever 代码的简短、可重复使用的版本。有关此课程的完整说明,请参阅这篇博文

第 3 步:使用 getOrAwaitValue 写入断言

在此步骤中,您将使用 getOrAwaitValue 方法并编写一个断言,以检查是否触发了 newTaskEvent

  1. 使用 getOrAwaitValue 获取 newTaskEventLiveData 值。
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. 断言值不为 null。
assertThat(value.getContentIfNotHandled(), (not(nullValue())))

完整的测试应如以下代码所示。

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()


    @Test
    fun addNewTask_setsNewTaskEvent() {
        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.getOrAwaitValue()

        assertThat(value.getContentIfNotHandled(), not(nullValue()))


    }

}
  1. 运行您的代码,然后观察测试通过情况!

现在,您已了解了如何编写测试、自行编写一个测试。在此步骤中,您将使用您学到的技能练习再编写一个 TasksViewModel 测试。

第 1 步:编写您自己的 ViewModel 测试

您将写setFilterAllTasks_tasksAddViewVisible()。此测试应检查您是否设置了过滤器类型以显示所有任务,以确保添加任务按钮可见。

  1. 使用 addNewTask_setsNewTaskEvent() 作为参考,在 TasksViewModelTest 中编写一个名为 setFilterAllTasks_tasksAddViewVisible() 的测试,将过滤模式设置为 ALL_TASKS,并断言 tasksAddViewVisible LiveData 为 true


请使用下方的代码开始创建。

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel

        // When the filter type is ALL_TASKS

        // Then the "Add task" action is visible
        
    }

注意:

  • 所有任务的 TasksFilterType 枚举均为 ALL_TASKS.
  • 添加任务的按钮的显示设置由 LiveData tasksAddViewVisible. 控制
  1. 运行测试。

第 2 步:将测试与解决方案进行比较

将您的解决方案与下面的解决方案进行比较。

TasksViewModelTest

    @Test
    fun setFilterAllTasks_tasksAddViewVisible() {

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
    }

检查您是否需要执行以下操作:

  • 您可以使用相同的 AndroidX ApplicationProvider.getApplicationContext() 语句创建 tasksViewModel
  • 您调用 setFiltering 方法,并传入 ALL_TASKS 过滤条件类型枚举。
  • 使用 getOrAwaitNextValue 方法检查 tasksAddViewVisible 是否为 true。

第 3 步:添加 @Before 规则

请注意,在两个测试开始时,您都定义了 TasksViewModel

TasksViewModelTest

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

在为多个测试重复设置代码后,您可以使用 @Before 注解来创建设置方法并移除重复的代码。由于所有这些测试都将测试 TasksViewModel,并且需要视图模型,因此请将此代码移至 @Before 代码块。

  1. 创建一个名为 tasksViewModel|lateinit 实例变量。
  2. 创建一个名为 setupViewModel 的方法。
  3. 为该文件添加 @Before 注解。
  4. 将视图模型实例化代码移至 setupViewModel

TasksViewModelTest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. 运行您的代码!

警告

请勿执行以下操作,也不要初始化

tasksViewModel

定义:

val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

这会导致所有测试使用相同的实例。这是您应避免的,因为每个测试都应该有一个新的受测对象实例(在本例中为 ViewModel)。

TasksViewModelTest 的最终代码应类似于以下代码。

TasksViewModelTest

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    // Executes each task synchronously using Architecture Components.
    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }


    @Test
    fun addNewTask_setsNewTaskEvent() {

        // When adding a new task
        tasksViewModel.addNewTask()

        // Then the new task event is triggered
        val value = tasksViewModel.newTaskEvent.awaitNextValue()
        assertThat(
            value?.getContentIfNotHandled(), (not(nullValue()))
        )
    }

    @Test
    fun getTasksAddViewVisible() {

        // When the filter type is ALL_TASKS
        tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)

        // Then the "Add task" action is visible
        assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
    }
    
}

点击此处,查看开始代码和最终代码之间的差异。

如需下载已完成的 Codelab 的代码,您可以使用下面的 git 命令:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1


另一种方法是,以 Zip 文件的形式下载代码库,将其解压缩,然后在 Android Studio 中打开它。

下载 Zip 文件

此 Codelab 涵盖以下内容:

  • 如何从 Android Studio 运行测试。
  • 本地测试 (test) 和插桩测试 (androidTest) 之间的区别。
  • 如何使用 JUnitHamcrest 编写本地单元测试。
  • 使用 AndroidX Test Library 设置 ViewModel 测试。

Udacity 课程:

Android 开发者文档:

视频:

其他:

如需查看本课程中其他 Codelab 的链接,请参阅“使用 Kotlin 进行高级 Android 开发”的 Codelab 着陆页。