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 文件中定义。导航在 ViewModel 中使用Event类触发;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 测试之间的区别在于它们的运行方式。

本地测试(test 源集)

这些测试在您的开发机器的 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. 编写测试,使用 Given、When、Then 结构,并使用遵循约定的名称。
  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 可以根据测试是 Instrumentation 测试还是本地测试以不同的方式运行你的测试。

4820a5757fd79a44.png

步骤 6. 修复 Robolectric 警告

当你运行代码时,会注意到使用了 Robolectric。

b10f151c068efc90.png

由于 AndroidX Test 和 AndroidJunit4 测试运行器,这一切都无需你手动编写一行 Robolectric 代码就能完成!

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

  • 找不到清单文件:./AndroidManifest.xml
  • "WARN: Android SDK 29 需要 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。

总结

  • 纯 ViewModel 测试通常可以放在 test 源集下,因为它们的代码通常不需要 Android。
  • 你可以使用 AndroidX 测试库 获取组件(如 Application 和 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 规则。当你使用 @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 的预期行为,你需要使用 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.
  • 添加任务按钮的可见性由 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. 将视图模型实例化代码移至 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. 下一个代码实验室

开始下一课: