Kotlin 进阶 Android 开发 05.1:测试基础

1. 欢迎

简介

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

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

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

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

您应该了解的知识

您应该熟悉以下内容:

您将学到什么

您将学习以下主题:

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

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

您将做什么

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

2. 应用概述

在本系列 Codelab 中,您将使用 TO-DO Notes 应用。该应用允许您记下要完成的任务并将其显示在列表中。然后,您可以将它们标记为已完成或未完成,对其进行过滤或删除它们。

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:运行示例应用

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

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

483916536f10c42a.png

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

TO-DO 应用基于架构蓝图测试和架构示例。该应用遵循应用架构指南中的架构。它使用带有 Fragment 的 ViewModel、存储库和 Room。如果您熟悉以下任何示例,则此应用具有类似的架构:

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

f2e425a052f7caf7.png

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

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

.addedittask

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

.data

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

.statistics

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

.taskdetail

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

.tasks

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

.util

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

数据层 (.data)

此应用包括一个模拟的网络层(在远程软件包中)和一个数据库层(在本地软件包中)。为简单起见,在此项目中,网络层仅使用带有延迟的HashMap进行模拟,而不是发出真实的网络请求。

DefaultTasksRepository协调或在网络层和数据库层之间进行中介,并向 UI 层返回数据。

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

每个 UI 层软件包都包含一个 Fragment 和一个 ViewModel,以及 UI 所需的任何其他类(例如任务列表的适配器)。TaskActivity是包含所有 Fragment 的 Activity。

导航

应用的导航由导航组件控制。它在nav_graph.xml文件中定义。导航是使用Event类在 ViewModel 中触发的;ViewModel 还会确定要传递的参数。Fragment 观察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)

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

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

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

本地测试(测试源集)

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

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

9060ac11ceb5e66e.png

Instrumentation 测试(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 进行测试(在本 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. 运行测试。
  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 库。您将在本 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 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 的所有来龙去脉,因此,如果您想了解更多信息,请查看 官方教程

subjectUnderTest_actionOrInput_resultState

  • 正在测试的主题是正在测试的方法或类(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 根据测试是检测测试还是本地测试以不同的方式运行测试。

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。

总而言之

  • 纯视图模型测试通常可以放在 test 源代码集中,因为它们的代码通常不需要 Android。
  • 您可以使用 AndroidX 测试库 获取应用程序和活动等组件的测试版本。

  • 如果您需要在 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 规则。当您将其与 @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 事件。
  • 触发任何 转换

要获得 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()
            [email protected](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.
  • 添加任务按钮的可见性由 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 过滤器类型枚举。
  • 您使用 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 测试库 设置 ViewModel 测试。

13. 了解更多

示例

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

Udacity 课程

Android 开发者文档

视频

其他

开始下一课:

14. 下一个代码实验室

开始下一课: