Kotlin 高级 Android 开发 05.1:测试基础

1. 欢迎

简介

在您实现第一个应用程序的第一个功能时,您可能运行了代码以验证它是否按预期工作。您进行了一次**测试**,尽管是**手动测试**。随着您继续添加和更新功能,您可能也继续运行代码并验证其是否有效。但是每次手动执行此操作都非常费力,容易出错,而且无法扩展。

计算机擅长扩展和自动化!因此,大大小小的公司中的开发人员都会编写**自动化测试**,这些测试由软件运行,不需要您手动操作应用程序来验证代码是否有效。

在本系列 Codelab 中,您将学习如何为一个真实的应用程序创建一个测试集合(称为**测试套件**)。

本第一个 Codelab 涵盖了 Android 测试的基础知识,您将编写您的第一个测试,并学习如何测试LiveDataViewModel

您应该了解的内容

您应该熟悉:

您将学习的内容

您将学习以下主题:

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

您将学习以下库和代码概念:

您将执行的操作

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

2. 应用程序概述

在本系列 Codelab 中,您将使用待办事项应用程序。该应用程序允许您记下要完成的任务并将其显示在列表中。然后,您可以将其标记为已完成或未完成,对其进行筛选或删除。

e490df637e1bf10c.gif

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

3. 开始

要开始,请下载代码

或者,您可以克隆代码的 Github 存储库

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

您可以在android-testing Github 存储库中浏览代码。

4. 任务:熟悉代码

在此任务中,您将运行应用程序并浏览代码库。

步骤 1:运行示例应用程序

下载待办事项应用程序后,在 Android Studio 中打开它并运行它。它应该可以编译。通过执行以下操作来浏览应用程序:

  • 使用加号浮动操作按钮创建一个新任务。先输入标题,然后输入有关任务的其他信息。使用绿色复选标记 FAB 保存它。
  • 在任务列表中,点击您刚刚完成的任务的标题,然后查看该任务的详细信息屏幕以查看其余说明。
  • 在列表中或详细信息屏幕上,选中该任务的复选框以将其状态设置为**已完成**。
  • 返回任务屏幕,打开筛选菜单,按**活动**和**已完成**状态筛选任务。
  • 打开导航抽屉并点击**统计信息**。
  • 返回概述屏幕,然后从导航抽屉菜单中选择**清除已完成**以删除所有状态为**已完成**的任务

483916536f10c42a.png

步骤 2:浏览示例应用程序代码

待办事项应用程序基于架构蓝图测试和架构示例。该应用程序遵循应用程序架构指南中的架构。它使用带有片段的 ViewModel、存储库和 Room。如果您熟悉以下任何示例,则此应用程序具有类似的架构:

您理解应用程序的总体架构比深入理解任何一层中的逻辑更重要。

f2e425a052f7caf7.png

以下是您将找到的软件包摘要:

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

.addedittask

添加或编辑任务屏幕:添加或编辑任务的 UI 层代码。

.data

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

.statistics

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

.taskdetail

任务详细信息屏幕:单个任务的 UI 层代码。

.tasks

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

.util

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

数据层 (.data)

此应用程序包含一个模拟网络层(在**remote**包中)和一个数据库层(在**local**包中)。为简便起见,在此项目中,网络层使用带有延迟的HashMap模拟,而不是发出实际的网络请求。

DefaultTasksRepository协调或仲裁网络层和数据库层之间的关系,并返回数据到 UI 层。

UI 层(.addedittask、.statistics、.taskdetail、.tasks)

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

导航

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

5. 任务:运行测试

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

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

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

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

本地测试工具测试之间的区别在于它们的运行方式。

本地测试(test 源集)

这些测试在您的开发机器的 JVM 上本地运行,不需要模拟器或物理设备。因此,它们的运行速度很快,但保真度较低,这意味着它们的运行方式与现实世界中的运行方式不太相似。

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

9060ac11ceb5e66e.png

工具测试(androidTest 源集)

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

在 Android Studio 中,已 instrumentation 测试用一个带有绿色和红色三角形图标的 Android 图标表示。

6df04e9088327cf8.png

步骤 1:运行本地测试

  1. 打开 test 文件夹,直到找到 **ExampleUnitTest.kt** 文件。
  2. 右键单击它并选择 **运行 ExampleUnitTest**。

您应该在屏幕底部的 **运行** 窗口中看到以下输出

cb237d020d5ed709.png

  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 进行测试(在此代码实验室中为 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. 运行测试。
  2. 在测试结果中,请注意测试旁边的 X。

e80a71def694097f.png

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

步骤 3:运行 instrumentation 测试

Instrumentation 测试位于 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 手机)

cbc15c3229c7deec.png

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

6. 任务:编写您的第一个测试

在此任务中,您将为 getActiveAndCompleteStats 编写测试,该函数计算应用程序的活动和已完成任务统计数据的百分比。您可以在应用程序的统计信息屏幕上看到这些数字。

7abfbf08efb1b623.png

步骤 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 并选择 **生成** > **测试**。

将打开 **创建测试** 对话框

1eb4d2bcea2a5323.png

  1. 将 **类名:** 更改为 StatisticsUtilsTest(而不是 StatisticsUtilsKtTest;在测试类名中没有 KT 更好一些)。
  2. **保留其余默认值。**JUnit 4 是合适的测试库。目标包是正确的(它反映了 StatisticsUtils 类的位置),您无需选中任何复选框(这只会生成额外的代码,但您将从头开始编写测试)。
  3. 按 **确定**。

将打开 **选择目标目录** 对话框:3342fe4d590f2129.png

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

  1. 选择 **test** 目录(而不是 androidTest),因为您将编写本地测试。
  2. 单击 **确定**。
  3. 请注意,生成的 StatisticsUtilsTest 类位于 test/statistics/ 中。

2fcd839638adcdfc.png

步骤 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 并选择 **运行**)。

它应该通过

2d4423767733aac8.png

步骤 3:添加 Hamcrest 依赖项

因为您的测试充当代码功能的文档,所以当它们易于人类阅读时会更好。比较以下两个断言

assertEquals(result.completedTasksPercent, 0f)

// versus

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

第二个断言更像是一句人话。它使用名为 Hamcrest 的断言框架编写。另一个用于编写易于阅读的断言的好工具是 Truth 库。您将在本代码实验室中使用 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 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. 运行更新后的测试以确认它仍然有效!

本代码实验室不会教您 Hamcrest 的所有来龙去脉,因此,如果您想了解更多信息,请查看 官方教程

被测对象_动作或输入_结果状态

  • 被测对象是被测试的方法或类(getActiveAndCompletedStats)。
  • 接下来是动作或输入(noCompleted)。
  • 最后是预期结果(returnsHundredZero)。

7. 任务:编写更多测试

这是一个可选的练习任务。

在此任务中,你将使用 JUnit 和 Hamcrest 编写更多测试。你还会使用源自程序实践的测试驱动开发策略编写测试。测试驱动开发或 TDD 是一种编程思想,它认为与其先编写功能代码,不如先编写测试。然后,你的目标是编写功能代码以通过测试。

步骤 1. 编写测试

编写当你拥有普通任务列表时的测试

  1. 如果有一个已完成的任务而没有活动的任务,则activeTasks百分比应为0f,已完成的任务百分比应为100f
  2. 如果有两个已完成的任务和三个活动的任务,则已完成的百分比应为40f,活动百分比应为60f

步骤 2. 为错误编写测试

已编写的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. 对所有测试重复此步骤!

5408e4b11ef2d3eb.png

与其先修复错误,不如先编写测试。然后,你可以确认你拥有防止将来意外重新引入这些错误的测试。

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

c7952b977e893441.png

步骤 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. 再次运行你的测试并确认所有测试现在都通过!

cf077166a26ac325.png

通过遵循 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测试。

8. 任务:使用 AndroidX Test 设置 ViewModel 测试

在代码实验室的其余部分,你将学习如何为大多数应用程序中常见的两个 Android 类编写测试 - ViewModelLiveData

你首先为TasksViewModel编写测试。

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

16d3cf02ddd17181.png

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

TasksViewModel.kt

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

在这种情况下,newTaskEvent表示已按下加号 FAB,你应该转到AddEditTaskFragment。你可以在这里了解更多关于事件的信息,也可以在这里了解更多关于事件的信息

步骤 1. 创建 TasksViewModelTest 类

按照你对StatisticsUtilTest所做的步骤,在此步骤中,你将为TasksViewModelTest创建一个测试文件。

  1. 打开你想要测试的类,在tasks包中,TasksViewModel。
  2. 在代码中,右键单击类名TasksViewModel -> 生成 -> 测试

61f8170f7ba50cf6.png

  1. 创建测试屏幕上,单击确定以接受(无需更改任何默认设置)。
  2. 选择目标目录对话框中,选择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的实例进行测试时,其构造函数需要一个应用程序上下文。但在此测试中,你没有创建具有活动和 UI 和片段的完整应用程序,那么你如何获取应用程序上下文呢?

TasksViewModelTest.kt

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

AndroidX Test 库包含提供应用程序和活动等组件版本的类和方法,这些组件适用于测试。当你拥有需要模拟 Android 框架类(例如应用程序上下文)的本地测试时,请按照以下步骤正确设置 AndroidX Test

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

你将完成这些步骤,然后了解它们一起如何工作。

步骤 3. 添加 Gradle 依赖项

  1. 将这些依赖项复制到你的应用程序模块的build.gradle文件中,以添加核心 AndroidX Test 核心和扩展依赖项以及 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

@Config(sdk = [30]) // Remove when Robolectric supports SDK 31
@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 是一个用于测试的库集合。它包含提供应用程序和活动等组件版本的类和方法,这些组件适用于测试。例如,你编写的代码是用于获取应用程序上下文的 AndroidX Test 函数的示例。

ApplicationProvider.getApplicationContext()

AndroidX Test API 的好处之一是它们被构建为同时适用于本地测试检测测试。这很好,因为

  • 你可以将相同的测试作为本地测试或检测测试运行。
  • 你无需学习本地测试与检测测试的不同测试 API。

例如,因为你使用 AndroidX Test 库编写了代码,所以你可以将TasksViewModelTest类从test文件夹移动到androidTest文件夹,测试仍然会运行。getApplicationContext()的工作方式略有不同,具体取决于它是作为本地测试还是检测测试运行

  • 如果它是检测测试,它将获得启动模拟器或连接到真实设备时提供的实际应用程序上下文。
  • 如果它是本地测试,它将使用模拟的 Android 环境。

什么是 Robolectric?

AndroidX Test 用于本地测试的模拟 Android 环境由Robolectric提供。Robolectric是一个库,它为测试创建模拟的 Android 环境,并且比启动模拟器或在设备上运行的速度更快。如果没有 Robolectric 依赖项,你将收到此错误

d1001b21699f5c2f.png

@RunWith(AndroidJUnit4::class)的作用是什么?

测试运行器是一个运行测试的JUnit组件。如果没有测试运行器,您的测试将无法运行。JUnit提供了一个默认的测试运行器,您可以自动获得。 @RunWith 将替换该默认测试运行器。

AndroidJUnit4 测试运行器允许 AndroidX Test 根据测试是Instrumented测试还是本地测试以不同的方式运行您的测试。

4820a5757fd79a44.png

步骤 6. 修复 Robolectric 警告

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

b10f151c068efc90.png

由于使用了 AndroidX Test 和 AndroidJunit4 测试运行器,无需您直接编写一行 Robolectric 代码即可完成此操作!

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

  • 找不到清单文件: ./AndroidManifest.xml
  • “警告:Android SDK 29 需要 Java 9...”

您可以通过更新您的 gradle 文件来修复找不到清单文件: ./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

        // ... 
    }

警告“警告:Android SDK 29 需要 Java 9...” 更复杂。 在 Android Q 上运行测试需要 Java 9。对于此代码实验室,请将您的目标和编译 SDK 保持在 28,而不是尝试配置 Android Studio 以使用 Java 9。

总结

  • 纯 ViewModel 测试通常可以放在test 源集,因为它们的代码通常不需要 Android。
  • 您可以使用 AndroidX 测试库 获取应用程序和 Activity 等组件的测试版本。
  • 如果您需要在test 源集中运行模拟的 Android 代码,您可以添加 Robolectric 依赖项和@RunWith(AndroidJUnit4::class) 注解。

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

9. 任务:为 LiveData 编写断言

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

这是您在没有addNewTask_setsNewTaskEvent ViewModel 测试的情况下停止的地方。

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 Rule。当您将其与@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上设置活动的观察者才能

要获得 ViewModel 的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 文件。

55518dc429736238.png

  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. 断言该值不为空。
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. 运行您的代码并观察测试通过!

10. 任务:编写多个 ViewModel 测试

既然您已经了解了如何编写测试,请自己编写一个测试。在此步骤中,使用您所学的技能,练习编写另一个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.
  • 添加任务的按钮的可见性由LiveDatatasksAddViewVisible.控制。
  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过滤器类型枚举。
  • 您使用getOrAwaitValue方法检查tasksAddViewVisible是否为 true。

步骤 3. 添加 @Before 规则

请注意,在您的两个测试的开头,您是如何定义TasksViewModel的。

TasksViewModelTest

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

当您有多个测试的重复设置代码时,您可以使用@Before注解来创建一个设置方法并删除重复的代码。由于所有这些测试都将测试TasksViewModel并且需要一个 ViewModel,因此请将此代码移动到@Before块。

  1. 创建一个名为tasksViewModel|lateinit实例变量。
  2. 创建一个名为setupViewModel的方法。
  3. 使用@Before对其进行注释。
  4. 将 ViewModel 实例化代码移动到setupViewModel

TasksViewModelTest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

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

您的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.getOrAwaitValue()
        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.getOrAwaitValue(), `is`(true))
    }
    
}

11.解决方案代码

点击这里查看您开始使用的代码和最终代码之间的差异。

要下载完成的代码实验室的代码,您可以使用下面的 git 命令

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

或者,您可以将存储库下载为 Zip 文件,解压缩它,然后在 Android Studio 中打开它。

12. 总结

本代码实验室涵盖了:

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

13. 了解更多

示例

  • 官方架构示例 - 这是官方架构示例,它基于此处使用的相同的待办事项应用程序。此示例中的概念超出了三个测试代码实验室中涵盖的内容。
  • Sunflower 演示 - 这是主要的 Android Jetpack 示例,它也使用了 Android 测试库

Udacity 课程

Android 开发者文档

视频

其他

开始下一课:

14. 下一个代码实验室

开始下一课: