1. 欢迎
简介
当您实现第一个应用的首个功能时,您可能会运行代码以验证其是否按预期工作。您执行了测试,尽管是手动测试。随着您不断添加和更新功能,您可能也继续运行代码并验证其是否工作。但每次都手动执行此操作会很累,容易出错,并且无法扩展。
计算机在扩展和自动化方面表现出色!因此,无论公司大小,开发人员都会编写自动化测试,这些测试由软件运行,无需您手动操作应用即可验证代码是否工作。
在本系列 Codelab 中,您将学习如何为实际应用创建一组测试(称为测试套件)。
第一个 Codelab 涵盖 Android 测试的基础知识,您将编写第一个测试并学习如何测试 LiveData
和 ViewModel
。
您应该已经掌握的知识
您应熟悉以下内容:
- 以下核心 Android Jetpack 库:
ViewModel
和LiveData
- 应用架构,遵循应用架构指南和Android 基础 Codelab 中的模式
您将学到的知识
您将学习以下主题:
- 如何在 Android 上编写和运行单元测试
- 如何使用测试驱动开发
- 如何选择仪器化测试和本地测试
您将学习以下库和代码概念:
您将执行的操作
- 在 Android 中设置、运行和解读本地测试和仪器化测试。
- 使用 JUnit4 和 Hamcrest 在 Android 中编写单元测试。
- 编写简单的
LiveData
和ViewModel
测试。
2. 应用概览
在本系列 Codelab 中,您将使用待办事项笔记应用。该应用可让您记下要完成的任务,并以列表形式显示。然后,您可以将它们标记为已完成或未完成、进行过滤或删除。
此应用使用 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 中打开并运行它。它应该可以编译。通过执行以下操作来探索应用:
- 使用加号浮动操作按钮创建一个新任务。首先输入标题,然后输入有关任务的附加信息。使用绿色勾选浮动操作按钮保存它。
- 在任务列表中,点击您刚刚完成的任务的标题,查看该任务的详细信息屏幕,以查看其余的描述。
- 在列表或详细信息屏幕上,勾选该任务的复选框,将其状态设置为已完成。
- 返回任务屏幕,打开过滤菜单,并按活跃和已完成状态过滤任务。
- 打开导航抽屉,点击统计信息。
- 返回概览屏幕,然后从导航抽屉菜单中选择清除已完成以删除所有状态为已完成的任务。
步骤 2:探索示例应用代码
待办事项应用基于架构蓝图测试和架构示例。该应用遵循应用架构指南中的架构。它使用带 Fragment 的 ViewModel、仓库和 Room。如果您熟悉以下任何示例,此应用都具有类似的架构:
- Android Kotlin 基础训练 Codelab
- Android 进阶训练 Codelab
- Room with a View Codelab
- Android Sunflower 示例
- 使用 Kotlin 开发 Android 应用 Udacity 培训课程
更重要的是您理解应用的总体架构,而不是深入理解任何一层的逻辑。
以下是您将找到的包摘要:
包: | ||
| 添加或编辑任务屏幕:用于添加或编辑任务的 UI 层代码。 | |
| 数据层:这处理任务的数据层。它包含数据库、网络和仓库代码。 | |
| 统计信息屏幕:统计信息屏幕的 UI 层代码。 | |
| 任务详细信息屏幕:单个任务的 UI 层代码。 | |
| 任务屏幕:所有任务列表的 UI 层代码。 | |
| 工具类:在应用各个部分使用的共享类,例如用于多个屏幕上使用的滑动刷新布局。 |
数据层 (.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. 任务:运行测试
在此任务中,您将运行您的第一个测试。
- 在 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 中,本地测试由绿色和红色三角形图标表示。
仪器化测试 (androidTest 源集)
这些测试在真实或模拟的 Android 设备上运行,因此它们能反映真实世界中会发生的情况,但速度也慢得多。
在 Android Studio 中,仪器化测试由带有绿色和红色三角形图标的 Android 图标表示。
步骤 1:运行本地测试
- 打开
test
文件夹,直到找到 ExampleUnitTest.kt 文件。 - 右键点击它并选择运行 ExampleUnitTest。
您应该在屏幕底部的运行窗口中看到以下输出:
- 请注意绿色勾选标记,并展开测试结果以确认名为
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。
- 将
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
}
}
- 运行测试。
- 在测试结果中,请注意测试旁边的 X。
- 另请注意:
- 一个失败的断言会使整个测试失败。
- 您将看到期望值 (3) 与实际计算值 (2)。
- 系统将引导您到失败断言的行
(ExampleUnitTest.kt:16)
。
步骤 3:运行仪器化测试
仪器化测试位于 androidTest
源集中。
- 打开
androidTest
源集。 - 运行名为
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 手机)。
如果您连接了设备或正在运行模拟器,您应该会看到测试在模拟器上运行。
6. 任务:编写您的第一个测试
在此任务中,您将为 getActiveAndCompleteStats
编写测试,该函数计算您应用的活跃任务和已完成任务的统计百分比。您可以在应用的统计信息屏幕上看到这些数字。
步骤 1:创建测试类
- 在
main
源集中的todoapp.statistics
下,打开StatisticsUtils.kt
。 - 找到
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
函数接受任务列表并返回 StatsResult
。StatsResult
是一个数据类,包含两个数字:已完成任务的百分比和活跃任务的百分比。
Android Studio 为您提供工具来生成测试存根,以帮助您实现此函数的测试。
- 右键点击
getActiveAndCompletedStats
并选择生成 > 测试。
创建测试对话框打开
- 将类名:更改为
StatisticsUtilsTest
(而不是StatisticsUtilsKtTest
;测试类名中没有 KT 会稍微好一些)。 - 保留其余的默认设置。JUnit 4 是合适的测试库。目标包是正确的(它与
StatisticsUtils
类的位置相对应),并且您不需要勾选任何复选框(这只会生成额外的代码,但您将从头开始编写测试)。 - 点击确定。
选择目标目录对话框打开:
您将进行本地测试,因为您的函数正在执行数学计算,并且不包含任何 Android 特定代码。因此,无需在真实或模拟设备上运行它。
- 选择
test
目录(而不是androidTest
),因为您将编写本地测试。 - 点击确定。
- 请注意在
test/statistics/
中生成的StatisticsUtilsTest
类。
步骤 2:编写您的第一个测试函数
您将编写一个检查以下内容的测试:
- 如果没有已完成任务和一个活跃任务,
- 活跃任务的百分比是 100%,
- 已完成任务的百分比是 0%。
- 打开
StatisticsUtilsTest
。 - 创建一个名为
getActiveAndCompletedStats_noCompleted_returnsHundredZero
的函数。
StatisticsUtilsTest.kt
class StatisticsUtilsTest {
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task
// Call your function
// Check the result
}
}
- 在函数名称上方添加
@Test
注解,以表明它是一个测试。 - 创建任务列表。
// Create an active task
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
- 使用这些任务调用
getActiveAndCompletedStats
。
// Call your function
val result = getActiveAndCompletedStats(tasks)
- 使用断言检查
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)
}
}
- 运行测试(右键点击
StatisticsUtilsTest
并选择运行)。
它应该通过。
步骤 3:添加 Hamcrest 依赖项
因为您的测试可以作为代码功能的文档,所以它们具有人类可读性会更好。比较以下两个断言:
assertEquals(result.completedTasksPercent, 0f)
// versus
assertThat(result.completedTasksPercent, `is`(0f))
第二个断言读起来更像一个人类句子。它是使用名为 Hamcrest 的断言框架编写的。另一个编写可读断言的好工具是 Truth 库。在本 Codelab 中,您将使用 Hamcrest 编写断言。
- 打开
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 编写断言
- 更新
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))
}
}
- 运行您更新的测试,确认它仍然有效!
此 Codelab 不会教您 Hamcrest 的所有详细内容,因此如果您想了解更多信息,请查阅官方教程。
subjectUnderTest_actionOrInput_resultState
- 被测对象是正在测试的方法或类(
getActiveAndCompletedStats
)。 - 接下来是操作或输入(
noCompleted
)。 - 最后是期望结果(
returnsHundredZero
)。
7. 任务:编写更多测试
这是一个可选的练习任务。
在此任务中,您将使用 JUnit 和 Hamcrest 编写更多测试。您还将使用源自测试驱动开发编程实践的策略编写测试。测试驱动开发(或 TDD)是一种编程思想流派,它主张您首先编写测试,而不是首先编写功能代码。然后,您编写功能代码,目标是使您的测试通过。
步骤 1. 编写测试
为正常任务列表编写测试:
- 如果有一个已完成任务且没有活跃任务,则
activeTasks
百分比应为0f
,已完成任务百分比应为100f
。 - 如果有两个已完成任务和三个活跃任务,则已完成百分比应为
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()
)
}
要修复代码并编写测试,您将使用测试驱动开发。测试驱动开发遵循以下步骤:
- 编写测试,使用“给定、当、那么”结构,并使用符合约定的名称。
- 确认测试失败。
- 编写最少的代码以使测试通过。
- 对所有测试重复此操作!
您将首先编写测试,而不是先修复 bug。然后您可以确认有测试保护您,防止将来意外地再次引入这些 bug。
- 如果列表为空(
emptyList()
),则两个百分比都应为 0f。 - 如果加载任务时出错,列表将为
null
,并且两个百分比都应为 0f。 - 运行您的测试并确认它们失败。
步骤 3. 修复 bug
现在您已经有了测试,请修复这个 bug。
- 通过在
tasks
为null
或为空时返回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
)
}
}
- 再次运行您的测试,并确认所有测试现在都通过了!
通过遵循 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
)
}
}
恭喜您掌握了编写和运行测试的基础知识!接下来您将学习如何编写基本的 ViewModel
和 LiveData
测试。
8. 任务:使用 AndroidX Test 设置 ViewModel 测试
在 Codelab 的其余部分,您将学习如何为大多数应用中常见的两个 Android 类编写测试:ViewModel
和 LiveData
。
您首先为 TasksViewModel
编写测试。
您将专注于所有逻辑都在 ViewModel 中且不依赖仓库代码的测试。仓库代码涉及异步代码、数据库和网络调用,所有这些都会增加测试复杂性。您现在将避免这些,并专注于为不直接测试仓库中任何内容的 ViewModel 功能编写测试。
您将编写的测试将检查,当您调用 addNewTask
方法时,打开新任务窗口的 Event
是否被触发。这是您将要测试的应用代码。
TasksViewModel.kt
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
在这种情况下,newTaskEvent
表示加号浮动操作按钮已被按下,您应该转到 AddEditTaskFragment
。您可以在此处和此处了解有关事件的更多信息。
步骤 1. 创建 TasksViewModelTest 类
按照您为 StatisticsUtilTest
执行的相同步骤,在此步骤中,您为 TasksViewModelTest
创建一个测试文件。
- 打开您希望测试的类,在
tasks
包中的TasksViewModel
。 - 在代码中,右键点击类名
TasksViewModel
-> 生成 -> 测试。
- 在创建测试屏幕上,点击确定接受(无需更改任何默认设置)。
- 在选择目标目录对话框中,选择 test 目录。
步骤 2. 开始编写您的 ViewModel 测试
在此步骤中,您将添加一个 ViewModel 测试,以测试当您调用 addNewTask
方法时,打开新任务窗口的 Event
是否被触发。
- 创建一个名为
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:
- 添加 AndroidX Test 核心和扩展依赖项
- 添加 Robolectric 测试库依赖项
- 使用 AndroidJunit4 测试运行器注解该类
- 编写 AndroidX Test 代码
您将完成这些步骤,然后理解它们共同的作用。
步骤 3. 添加 Gradle 依赖项
- 将这些依赖项复制到您的应用模块的
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 测试运行器
- 在测试类上方添加
@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
,它获取一个应用上下文。
- 使用 AndroidX 测试库中的
ApplicationProvider.getApplicationContext()
创建一个TasksViewModel
。
TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
- 在
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
}
- 运行您的测试以确认它有效。
概念:AndroidX Test 如何工作?
什么是 AndroidX Test?
AndroidX Test 是一个测试库集合。它包含类和方法,为您提供用于测试的应用和 Activity 等组件版本。例如,您编写的这段代码就是用于获取应用上下文的 AndroidX Test 函数示例。
ApplicationProvider.getApplicationContext()
AndroidX Test API 的优点之一是它们既适用于本地测试也适用于仪器化测试。这很好,因为:
- 您可以将相同的测试作为本地测试或仪器化测试运行。
- 您无需学习用于本地测试与仪器化测试的不同测试 API。
例如,因为您使用 AndroidX Test 库编写了代码,您可以将您的 TasksViewModelTest
类从 test
文件夹移动到 androidTest
文件夹,测试仍将运行。getApplicationContext()
的工作方式略有不同,具体取决于它是作为本地测试还是仪器化测试运行:
- 如果它是仪器化测试,它将在启动模拟器或连接到真实设备时获取提供的实际应用上下文。
- 如果它是本地测试,它使用模拟的 Android 环境。
什么是 Robolectric?
AndroidX Test 用于本地测试的模拟 Android 环境由 Robolectric 提供。Robolectric 是一个库,它为测试创建模拟的 Android 环境,运行速度比启动模拟器或在设备上运行更快。如果没有 Robolectric 依赖项,您将收到此错误:
@RunWith(AndroidJUnit4::class)
有什么作用?
测试运行器是运行测试的 JUnit 组件。没有测试运行器,您的测试将无法运行。JUnit 提供了默认的测试运行器,您可以自动获得。@RunWith
替换了该默认测试运行器。
AndroidJUnit4
测试运行器允许 AndroidX Test 根据测试是仪器化测试还是本地测试来以不同方式运行您的测试。
步骤 6. 修复 Robolectric 警告
当您运行代码时,请注意使用了 Robolectric。
由于 AndroidX Test 和 AndroidJunit4 测试运行器,您无需直接编写一行 Robolectric 代码即可完成此操作!
您可能会注意到两个警告。
没有此类清单文件:./AndroidManifest.xml
“警告:Android SDK 29 需要 Java 9...”
您可以通过更新 Gradle 文件来修复 No such manifest file: ./AndroidManifest.AndroidManifest.xml
警告。
- 将以下行添加到您的 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
,建议您做两件事:
- 使用
InstantTaskExecutorRule
- 确保
LiveData
观察
步骤 1. 使用 InstantTaskExecutorRule
InstantTaskExecutorRule
是一个 JUnit 规则。当您将其与 @get:Rule
注解一起使用时,它会导致 InstantTaskExecutorRule
类中的某些代码在测试之前和之后运行(要查看确切的代码,您可以使用键盘快捷键 Command+B 查看文件)。
此规则在同一线程中运行所有与架构组件相关的后台任务,以便测试结果同步发生,并以可重复的顺序进行。当您编写包含 LiveData 测试的测试时,请使用此规则!
- 添加架构组件核心测试库(包含此规则)的 Gradle 依赖项。
app/build.gradle
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
- 打开
TasksViewModelTest.kt
- 在
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
的扩展函数,以简化观察者的添加。
- 在您的
test
源集中创建一个新的 Kotlin 文件,名为LiveDataTestUtil.kt
。
- 复制并粘贴以下代码。
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
}
这是一个相当复杂的方法。它创建一个名为 getOrAwaitValue
的Kotlin 扩展函数,该函数添加一个观察者,获取 LiveData
值,然后清理观察者——基本上是上面所示的 observeForever
代码的一个简短、可重用版本。有关此类的完整解释,请查看这篇博客文章。
步骤 3. 使用 getOrAwaitValue 编写断言
在此步骤中,您使用 getOrAwaitValue
方法并编写一个断言语句,检查 newTaskEvent
是否被触发。
- 使用
getOrAwaitValue
获取newTaskEvent
的LiveData
值。
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
- 断言该值不为 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()))
}
}
- 运行您的代码,并看到测试通过!
10. 任务:编写多个 ViewModel 测试
既然您已经了解如何编写测试,请尝试自己编写一个。在此步骤中,运用您学到的技能,练习编写另一个 TasksViewModel
测试。
步骤 1. 编写您自己的 ViewModel 测试
您将编写 setFilterAllTasks_tasksAddViewVisible()
。此测试应检查,如果您已将过滤器类型设置为显示所有任务,则添加任务按钮是否可见。
- 以
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
控制。
- 运行您的测试。
步骤 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
块中。
- 创建一个名为
tasksViewModel
的lateinit
实例变量。 - 创建一个名为
setupViewModel
的方法。 - 使用
@Before
注解它。 - 将 ViewModel 实例化代码移动到
setupViewModel
中。
TasksViewModelTest
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
- 运行您的代码!
您的 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
)之间的区别。 - 如何使用 JUnit 和 Hamcrest 编写本地单元测试。
- 使用AndroidX 测试库设置 ViewModel 测试。
13. 了解更多
示例
- 官方架构示例 - 这是官方架构示例,它基于此处使用的相同待办事项笔记应用。此示例中的概念超出了三个测试 Codelab 中涵盖的内容。
- Sunflower 演示 - 这是主要的 Android Jetpack 示例,也使用了 Android 测试库。
Udacity 课程
Android 开发者文档
视频
其他