(已弃用) 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 中打开并运行它。它应该可以编译。通过执行以下操作来探索应用:

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

483916536f10c42a.png

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

待办事项应用基于架构蓝图测试和架构示例。该应用遵循应用架构指南中的架构。它使用带 Fragment 的 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 层包都包含一个 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:包含称为仪器化测试的测试。
  • test:包含称为本地测试的测试。

本地测试仪器化测试的区别在于它们的运行方式。

本地测试(test 源集)

这些测试在您开发机器的 JVM 上本地运行,不需要模拟器或实体设备。因此,它们运行速度快,但逼真度较低,这意味着它们在实际环境中的行为方式不太相似。

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

9060ac11ceb5e66e.png

仪器化测试 (androidTest 源集)

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

在 Android Studio 中,仪器化测试由带有绿色和红色三角形图标的 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:运行仪器化测试

仪器化测试位于 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. 请注意在 test/statistics/ 中生成的 StatisticsUtilsTest 类。

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. 为一个 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. 编写测试,使用“给定、当、那么”结构,并使用符合约定的名称。
  2. 确认测试失败。
  3. 编写最少的代码以使测试通过。
  4. 对所有测试重复此操作!

5408e4b11ef2d3eb.png

您将首先编写测试,而不是先修复 bug。然后您可以确认有测试保护您,防止将来意外地再次引入这些 bug。

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

c7952b977e893441.png

步骤 3. 修复 bug

现在您已经有了测试,请修复这个 bug。

  1. 通过在 tasksnull 或为空时返回 0f 来修复 getActiveAndCompletedStats 中的 bug。
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 并首先编写测试,您有助于确保:

  • 新功能总是有相关的测试;因此您的测试可以作为代码功能的文档。
  • 您的测试检查正确的结果并防止您已经发现的 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 测试。

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

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

您首先为 TasksViewModel 编写测试。

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

16d3cf02ddd17181.png

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

TasksViewModel.kt

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

在这种情况下,newTaskEvent 表示加号浮动操作按钮已被按下,您应该转到 AddEditTaskFragment。您可以在此处此处了解有关事件的更多信息。

步骤 1. 创建 TasksViewModelTest 类

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

  1. 打开您希望测试的类,在 tasks 包中的 TasksViewModel
  2. 在代码中,右键点击类名 TasksViewModel -> 生成 -> 测试

61f8170f7ba50cf6.png

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

步骤 2. 开始编写您的 ViewModel 测试

在此步骤中,您将添加一个 ViewModel 测试,以测试当您调用 addNewTask 方法时,打开新任务窗口的 Event 是否被触发。

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

TasksViewModelTest.kt

class TasksViewModelTest {

    @Test
    fun addNewTask_setsNewTaskEvent() {

        // Given a fresh TasksViewModel


        // When adding a new task


        // Then the new task event is triggered

    }
    
}

那应用上下文呢?

当您创建一个 TasksViewModel 实例进行测试时,其构造函数需要一个应用上下文。但在此测试中,您并未创建带有 Activity、UI 和 Fragment 的完整应用,那么如何获取应用上下文呢?

TasksViewModelTest.kt

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

AndroidX Test 库包含类和方法,为您提供用于测试的应用和 Activity 等组件版本。当您进行本地测试,需要模拟 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 是一个测试库集合。它包含类和方法,为您提供用于测试的应用和 Activity 等组件版本。例如,您编写的这段代码就是用于获取应用上下文的 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 文件来修复 No such manifest file: ./AndroidManifest.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。在此 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 源集中创建一个新的 Kotlin 文件,名为 LiveDataTestUtil.kt

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. 断言该值不为 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. 运行您的代码,并看到测试通过!

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. 创建一个名为 tasksViewModellateinit 实例变量。
  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. 解决方案代码

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

要下载已完成 Codelab 的代码,您可以使用下面的 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. 总结

此 Codelab 涵盖了:

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

13. 了解更多

示例

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

Udacity 课程

Android 开发者文档

视频

其他

开始下一课:

14. 下一个 Codelab

开始下一课: