测试基础知识

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

简介

在实现第一个应用的第一个功能时,您可能运行了代码来验证该功能是否按预期运行。您已经执行了一项测试,尽管是手动测试。随着您不断添加和更新功能,您可能还会继续运行代码并验证其是否正常运行。但每次都手动执行此操作会很累,容易出错,而且无法扩缩。

计算机非常擅长扩缩和自动化!因此,无论公司规模大小,开发者都会编写自动化测试,这些测试由软件运行,无需您手动操作应用来验证代码是否正常运行。

在此系列 Codelab 中,您将学习如何为真实的应用创建一组测试(称为测试套件)。

在此第一个 Codelab 中,您将了解 Android 测试的基础知识,编写您的第一个测试,并学习如何测试 LiveDataViewModel

您应当已掌握的内容

您应熟悉以下内容:

学习内容

您将了解以下主题:

  • 如何在 Android 上编写和运行单元测试
  • 如何使用测试驱动开发
  • 如何选择插桩测试和本地测试

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

您将执行的操作

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

在此系列 Codelab 中,您将使用“待办事项记事”应用。该应用可让您记下要完成的任务,并以列表形式显示这些任务。然后,您可以将它们标记为已完成或未完成、过滤它们或删除它们。

此应用采用 Kotlin 编写,包含多个界面,使用 Jetpack 组件,并遵循应用架构指南中的架构。通过学习如何测试此应用,您将能够测试使用相同库和架构的应用。

首先,请下载代码:

下载 Zip 文件

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

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

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

第 1 步:运行示例应用

下载待办事项应用后,在 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:包含您的应用代码。此代码可在您构建的所有不同版本的应用(称为 build 变体)之间共享
  • 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. addition_isCorrect 测试添加了 assertEquals(3, 1 + 1)

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

系统会打开创建测试对话框:

  1. 类名称更改为 StatisticsUtilsTest(而不是 StatisticsUtilsKtTest;最好不要在测试类名称中包含 KT)。
  2. 保留其余默认设置。JUnit 4 是合适的测试库。目标软件包正确(与 StatisticsUtils 类的位置相同),并且您无需选中任何复选框(这只会生成额外的代码,但您将从头开始编写测试)。
  3. OK

系统会打开选择目标目录对话框:

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

  1. 选择 test 目录(而非 androidTest),因为您将编写本地测试。
  2. 点击确定
  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,然后选择运行)。

它应该通过:

第 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。当您准备好与全世界分享您的应用时,最好不要让应用中的任何测试代码或依赖项使 APK 的大小过大。您可以使用 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 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 代码存在 bug。请注意,它无法正确处理列表为空或为 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. 使用 Given、When、Then 结构编写测试,并使用符合惯例的名称。
  2. 确认测试失败。
  3. 编写最少的代码,使测试通过。
  4. 对所有测试重复此操作!

您将先编写测试,而不是先修复 bug。然后,您可以确认自己已通过测试来防止日后意外重新引入这些 bug。

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

第 3 步:修复 bug

现在,您已经有了测试,接下来请修复 bug。

  1. 修复了 getActiveAndCompletedStats 中的 bug,如果 tasksnull 或为空,则返回 0f
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 并先编写测试,您有助于确保:

  • 新功能始终有相关联的测试;因此,您的测试可作为代码功能的文档。
  • 测试会检查结果是否正确,并防范您之前遇到的 bug。

解决方案:编写更多测试

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

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 的其余部分,您将学习如何为大多数应用中常见的两个 Android 类(ViewModelLiveData)编写测试。

首先,您需要为 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 的实例以进行测试时,其构造函数需要 Application Context。不过,在此测试中,您不会创建包含 activity、界面和 fragment 的完整应用,那么如何获取应用 context 呢?

TasksViewModelTest.kt

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

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

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

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

第 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 中,请将目标 SDK 和编译 SDK 保持在 28,而不要尝试将 Android Studio 配置为使用 Java 9。

总结:

  • 纯视图模型测试通常可以放在 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

InstantTaskExecutorRule 是一种 JUnit 规则。当您将其与 @get:Rule 注释搭配使用时,它会导致 InstantTaskExecutorRule 类中的一些代码在测试之前和之后运行(如需查看确切的代码,您可以使用键盘快捷键 Command+B 查看该文件)。

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

  1. 添加 Architecture Components 核心测试库(包含此规则)的 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 库设置 ViewModel 测试。

Udacity 课程:

Android 开发者文档:

视频:

其他:

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