1. 开始之前
本 Codelab 教您编写单元测试以测试 ViewModel
组件。您将为 Unscramble 游戏应用添加单元测试。Unscramble 应用是一款有趣的文字游戏,用户需要猜出一个乱序的单词,猜对即可获得分数。下图显示了应用的预览
在 编写自动化测试 Codelab 中,您学习了什么是自动化测试以及它们为何重要。您还学习了如何实现单元测试。
您学习了
- 自动化测试是验证另一段代码准确性的代码。
- 测试是应用开发流程的重要组成部分。通过持续地对您的应用运行测试,您可以在公开发布应用之前验证其功能行为和可用性。
- 使用单元测试,您可以测试函数、类和属性。
- 本地单元测试在您的工作站上执行,这意味着它们在开发环境中运行,无需 Android 设备或模拟器。换句话说,本地测试在您的计算机上运行。
在继续之前,请确保您已完成 编写自动化测试 和 Compose 中的 ViewModel 和状态 Codelab。
先决条件
- Kotlin 知识,包括函数、lambda 和无状态组合
- Jetpack Compose 布局构建的基本知识
- Material Design 的基本知识
- 如何实现 ViewModel 的基本知识
您将学到什么
- 如何在应用模块的
build.gradle.kts
文件中添加单元测试依赖项 - 如何创建测试策略以实现单元测试
- 如何使用 JUnit4 编写单元测试并了解测试实例生命周期
- 如何运行、分析和改进代码覆盖率
您将构建什么
- Unscramble 游戏应用的单元测试
您需要什么
- 最新版本的 Android Studio
获取起始代码
要开始,请下载起始代码
或者,您可以克隆代码的 GitHub 存储库
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout viewmodel
您可以在 Unscramble
GitHub 存储库中浏览代码。
2. 起始代码概述
在单元 2 中,您学习了将单元测试代码放在 src 文件夹下的 test 源代码集中,如下图所示
起始代码包含以下文件
WordsData.kt
:此文件包含用于测试的单词列表和一个getUnscrambledWord()
辅助函数,用于从乱序单词中获取未乱序的单词。您无需修改此文件。
3. 添加测试依赖项
在本 Codelab 中,您使用 JUnit 框架编写单元测试。要使用该框架,您需要将其作为依赖项添加到应用模块的 build.gradle.kts
文件中。
您使用 implementation
配置指定应用所需的依赖项。例如,要在您的应用中使用 ViewModel
库,您必须向 androidx.lifecycle:lifecycle-viewmodel-compose
添加依赖项,如下面的代码片段所示
dependencies {
...
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}
您现在可以在应用的源代码中使用此库,Android Studio 将帮助将其添加到生成的应用包文件 (APK) 文件中。但是,您不希望您的单元测试代码成为 APK 文件的一部分。测试代码不会添加用户会使用的任何功能,并且代码还会影响 APK 大小。您的测试代码所需的依赖项也是如此;您应该将它们分开。为此,您使用 testImplementation
配置,这表示该配置适用于本地测试源代码,而不是应用代码。
要向您的项目添加依赖项,请在 build.gradle.kts
文件的 dependencies 块中指定依赖项配置(例如 implementation
或 testImplementation
)。每个依赖项配置都为 Gradle 提供有关如何使用该依赖项的不同说明。
添加依赖项
- 打开位于 Project 窗格中 app 目录下的
app
模块的build.gradle.kts
文件。
- 在文件中,向下滚动直到找到
dependencies{}
块。使用testImplementation
配置为junit
添加依赖项。
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("junit:junit:4.13.2")
}
- 在 build.gradle.kts 文件顶部的通知栏中,单击 立即同步 以让导入和构建完成,如下面的屏幕截图所示
Compose 物料清单 (BOM)
Compose BOM 是管理 Compose 库版本的推荐方法。BOM 允许您通过仅指定 BOM 的版本来管理所有 Compose 库版本。
请注意 app
模块的 build.gradle.kts
文件中的依赖项部分。
// No need to copy over
// This is part of starter code
dependencies {
// Import the Compose BOM
implementation (platform("androidx.compose:compose-bom:2023.06.01"))
...
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
...
}
观察以下内容
- 未指定 Compose 库版本号。
- 使用
implementation platform("androidx.compose:compose-bom:2023.06.01")
导入 BOM
这是因为 BOM 本身具有指向不同 Compose 库的最新稳定版本的链接,这样它们可以很好地协同工作。在您的应用中使用 BOM 时,您无需向 Compose 库依赖项本身添加任何版本。当您更新 BOM 版本时,您正在使用的所有库都会自动更新到其新版本。
要将 BOM 与 compose 测试库(检测测试)一起使用,您需要导入 androidTestImplementation platform("androidx.compose:compose-bom:xxxx.xx.xx")
。您可以创建一个变量并将其重用于 implementation
和 androidTestImplementation
,如所示。
// Example, not need to copy over
dependencies {
// Import the Compose BOM
implementation(platform("androidx.compose:compose-bom:2023.06.01"))
implementation("androidx.compose.material:material")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-tooling-preview")
// ...
androidTestImplementation(platform("androidx.compose:compose-bom:2023.06.01"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
太棒了!您已成功将测试依赖项添加到应用并了解了 BOM。您现在可以添加一些单元测试了。
4. 测试策略
良好的测试策略围绕着覆盖代码的不同路径和边界。在最基本的层面上,您可以将测试分为三种情况:成功路径、错误路径和边界情况。
- 成功路径:成功路径测试(也称为快乐路径测试)侧重于测试正向流程的功能。正向流程是指没有异常或错误条件的流程。与错误路径和边界情况相比,创建成功路径的详尽场景列表比较容易,因为它们侧重于应用的预期行为。
Unscramble 应用中成功路径的一个示例是,当用户输入正确的单词并单击 提交 按钮时,分数、单词计数和乱序单词的正确更新。
- 错误路径:错误路径测试侧重于测试负向流程的功能,即检查应用如何响应错误条件或无效的用户输入。确定所有可能的错误流程非常具有挑战性,因为当预期行为未实现时,可能会有很多可能的结果。
一条一般的建议是列出所有可能的错误路径,为它们编写测试,并在发现不同的场景时让您的单元测试不断发展。
Unscramble 应用中错误路径的一个示例是,用户输入错误的单词并单击 提交 按钮,这会导致出现错误消息,并且分数和单词计数不会更新。
- 边界情况:边界情况侧重于测试应用中的边界条件。在 Unscramble 应用中,边界是在应用加载时检查 UI 状态以及用户玩最大数量的单词后的 UI 状态。
围绕这些类别创建测试场景可以作为测试计划的指南。
创建测试
一个好的单元测试通常具有以下四个属性
- 专注:它应该专注于测试一个单元,例如一段代码。这段代码通常是一个类或一个方法。测试应该范围狭窄,并专注于验证各个代码段的正确性,而不是同时验证多个代码段。
- 易于理解:代码应简单易懂。开发人员只需一眼就能立即理解测试背后的意图。
- 确定性:它应该始终通过或失败。在不更改任何代码的情况下,无论运行测试多少次,测试都应产生相同的结果。测试不应该出现不稳定情况,即在一次实例中失败而在另一次实例中通过,尽管代码没有修改。
- 自包含:它不需要任何人工交互或设置,并且可以独立运行。
成功路径
要为成功路径编写单元测试,您需要断言,假设已初始化 GameViewModel
的实例,当使用正确的猜测单词调用 updateUserGuess()
方法,然后调用 checkUserGuess()
方法时,则
- 正确的猜测将传递给
updateUserGuess()
方法。 - 调用
checkUserGuess()
方法。 score
和isGuessedWordWrong
状态的值更新正确。
完成以下步骤以创建测试
- 在测试源集中创建一个新的包
com.example.android.unscramble.ui.test
,并添加如下所示的文件
要为 GameViewModel
类编写单元测试,您需要该类的实例,以便您可以调用该类的方法并验证状态。
- 在
GameViewModelTest
类的主体中,声明一个viewModel
属性,并为其分配GameViewModel
类的实例。
class GameViewModelTest {
private val viewModel = GameViewModel()
}
- 要为成功路径编写单元测试,请创建一个
gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset()
函数,并使用@Test
注解。
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
}
}
- 导入以下内容
import org.junit.Test
要将正确的玩家单词传递给 viewModel.updateUserGuess()
方法,您需要从 GameUiState
中的乱序单词获取正确的解开单词。为此,首先获取当前的游戏 ui 状态。
- 在函数体中,创建一个
currentGameUiState
变量,并为其分配viewModel.uiState.value
。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
}
- 要获取正确的玩家猜测,请使用
getUnscrambledWord()
函数,该函数接收currentGameUiState.currentScrambledWord
作为参数并返回解开单词。将此返回值存储在一个名为correctPlayerWord
的新只读变量中,并为其分配getUnscrambledWord()
函数返回的值。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- 要验证猜测的单词是否正确,请添加对
viewModel.updateUserGuess()
方法的调用,并将correctPlayerWord
变量作为参数传递。然后添加对viewModel.checkUserGuess()
方法的调用以验证猜测。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
}
您现在可以断言游戏状态符合您的预期。
- 从
viewModel.uiState
属性的值中获取GameUiState
类的实例,并将其存储在currentGameUiState
变量中。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
}
- 要检查猜测的单词是否正确以及分数是否已更新,请使用
assertFalse()
函数验证currentGameUiState.isGuessedWordWrong
属性是否为false
,并使用assertEquals()
函数验证currentGameUiState.score
属性的值是否等于20
。
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
// Assert that checkUserGuess() method updates isGuessedWordWrong is updated correctly.
assertFalse(currentGameUiState.isGuessedWordWrong)
// Assert that score is updated correctly.
assertEquals(20, currentGameUiState.score)
}
- 导入以下内容
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
- 为了使值
20
可读且可重用,请创建一个伴生对象,并将20
分配给名为SCORE_AFTER_FIRST_CORRECT_ANSWER
的private
常量。使用新创建的常量更新测试。
class GameViewModelTest {
...
@Test
fun gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset() {
...
// Assert that score is updated correctly.
assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
}
companion object {
private const val SCORE_AFTER_FIRST_CORRECT_ANSWER = SCORE_INCREASE
}
}
- 运行测试。
测试应该通过,因为所有断言都是有效的,如下面的屏幕截图所示
错误路径
要为错误路径编写单元测试,您需要断言,当将不正确的单词作为参数传递给 viewModel.updateUserGuess()
方法并调用 viewModel.checkUserGuess()
方法时,则会发生以下情况
currentGameUiState.score
属性的值保持不变。currentGameUiState.isGuessedWordWrong
属性的值设置为true
,因为猜测错误。
完成以下步骤以创建测试
- 在
GameViewModelTest
类的主体中,创建一个gameViewModel_IncorrectGuess_ErrorFlagSet()
函数,并使用@Test
注解。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
}
- 定义一个
incorrectPlayerWord
变量,并为其分配"and"
值,该值不应存在于单词列表中。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
}
- 添加对
viewModel.updateUserGuess()
方法的调用,并将incorrectPlayerWord
变量作为参数传递。 - 添加对
viewModel.checkUserGuess()
方法的调用以验证猜测。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
}
- 添加一个
currentGameUiState
变量,并为其分配viewModel.uiState.value
状态的值。 - 使用断言函数断言
currentGameUiState.score
属性的值为0
且currentGameUiState.isGuessedWordWrong
属性的值设置为true
。
@Test
fun gameViewModel_IncorrectGuess_ErrorFlagSet() {
// Given an incorrect word as input
val incorrectPlayerWord = "and"
viewModel.updateUserGuess(incorrectPlayerWord)
viewModel.checkUserGuess()
val currentGameUiState = viewModel.uiState.value
// Assert that score is unchanged
assertEquals(0, currentGameUiState.score)
// Assert that checkUserGuess() method updates isGuessedWordWrong correctly
assertTrue(currentGameUiState.isGuessedWordWrong)
}
- 导入以下内容
import org.junit.Assert.assertTrue
- 运行测试以确认它通过。
边界情况
要测试 UI 的初始状态,您需要为 GameViewModel
类编写单元测试。测试必须断言,当 GameViewModel
初始化时,以下为真
currentWordCount
属性设置为1
。score
属性设置为0
。isGuessedWordWrong
属性设置为false
。isGameOver
属性设置为false
。
完成以下步骤以添加测试
- 创建一个
gameViewModel_Initialization_FirstWordLoaded()
方法,并使用@Test
注解。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
}
- 访问
viewModel.uiState.value
属性以获取GameUiState
类的初始实例。将其分配给一个新的只读变量gameUiState
。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
}
- 要获取正确的玩家单词,请使用
getUnscrambledWord()
函数,该函数接收gameUiState.currentScrambledWord
单词并返回解开单词。将返回值分配给一个名为unScrambledWord
的新只读变量。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
}
- 要验证状态是否正确,请添加
assertTrue()
函数以断言currentWordCount
属性设置为1
,并且score
属性设置为0
。 - 添加
assertFalse()
函数以验证isGuessedWordWrong
属性是否为false
以及isGameOver
属性是否设置为false
。
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
val unScrambledWord = getUnscrambledWord(gameUiState.currentScrambledWord)
// Assert that current word is scrambled.
assertNotEquals(unScrambledWord, gameUiState.currentScrambledWord)
// Assert that current word count is set to 1.
assertTrue(gameUiState.currentWordCount == 1)
// Assert that initially the score is 0.
assertTrue(gameUiState.score == 0)
// Assert that the wrong word guessed is false.
assertFalse(gameUiState.isGuessedWordWrong)
// Assert that game is not over.
assertFalse(gameUiState.isGameOver)
}
- 导入以下内容
import org.junit.Assert.assertNotEquals
- 运行测试以确认它通过。
另一个边界情况是测试用户猜测所有单词后的 UI 状态。您需要断言,当用户正确猜测所有单词时,以下为真
- 分数是最新的;
currentGameUiState.currentWordCount
属性等于MAX_NO_OF_WORDS
常量的值;currentGameUiState.isGameOver
属性设置为true
。
完成以下步骤以添加测试
- 创建一个
gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly()
方法,并使用@Test
注解。在方法中,创建一个expectedScore
变量,并为其分配0
。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
}
- 要获取初始状态,请添加一个
currentGameUiState
变量,并将viewModel.uiState.value
属性的值分配给该变量。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
}
- 要获取正确的玩家单词,请使用
getUnscrambledWord()
函数,该函数接收currentGameUiState.currentScrambledWord
单词并返回解开单词。将此返回值存储在一个名为correctPlayerWord
的新只读变量中,并为其分配getUnscrambledWord()
函数返回的值。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
}
- 要测试用户是否猜测了所有答案,请使用
repeat
块重复执行viewModel.updateUserGuess()
方法和viewModel.checkUserGuess()
方法MAX_NO_OF_WORDS
次。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
}
}
- 在
repeat
块中,将SCORE_INCREASE
常量的值添加到expectedScore
变量中,以断言分数在每个正确答案后都会增加。 - 添加对
viewModel.updateUserGuess()
方法的调用,并将correctPlayerWord
变量作为参数传递。 - 添加对
viewModel.checkUserGuess()
方法的调用以触发对用户猜测的检查。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
}
}
- 更新当前玩家单词,使用
getUnscrambledWord()
函数,该函数接收currentGameUiState.currentScrambledWord
作为参数并返回解开单词。将此返回值存储在一个名为correctPlayerWord
的新只读变量中。要验证状态是否正确,请添加assertEquals()
函数以检查currentGameUiState.score
属性的值是否等于expectedScore
变量的值。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
// Assert that after each correct answer, score is updated correctly.
assertEquals(expectedScore, currentGameUiState.score)
}
}
- 添加一个
assertEquals()
函数,用于断言currentGameUiState.currentWordCount
属性的值等于MAX_NO_OF_WORDS
常量的值,并且currentGameUiState.isGameOver
属性的值设置为true
。
@Test
fun gameViewModel_AllWordsGuessed_UiStateUpdatedCorrectly() {
var expectedScore = 0
var currentGameUiState = viewModel.uiState.value
var correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
repeat(MAX_NO_OF_WORDS) {
expectedScore += SCORE_INCREASE
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
// Assert that after each correct answer, score is updated correctly.
assertEquals(expectedScore, currentGameUiState.score)
}
// Assert that after all questions are answered, the current word count is up-to-date.
assertEquals(MAX_NO_OF_WORDS, currentGameUiState.currentWordCount)
// Assert that after 10 questions are answered, the game is over.
assertTrue(currentGameUiState.isGameOver)
}
- 导入以下内容
import com.example.unscramble.data.MAX_NO_OF_WORDS
- 运行测试以确认它通过。
测试实例生命周期概述
当你仔细观察测试中 viewModel
的初始化方式时,你可能会注意到,即使所有测试都使用了它,viewModel
也只初始化一次。此代码片段显示了 viewModel
属性的定义。
class GameViewModelTest {
private val viewModel = GameViewModel()
@Test
fun gameViewModel_Initialization_FirstWordLoaded() {
val gameUiState = viewModel.uiState.value
...
}
...
}
你可能会有以下疑问
- 这是否意味着同一个
viewModel
实例被所有测试重复使用? - 这会导致任何问题吗?例如,如果
gameViewModel_Initialization_FirstWordLoaded
测试方法在gameViewModel_CorrectWordGuessed_ScoreUpdatedAndErrorFlagUnset
测试方法之后执行,初始化测试会失败吗?
这两个问题的答案都是否定的。测试方法以隔离的方式执行,以避免可变测试实例状态带来的意外副作用。默认情况下,在每个测试方法执行之前,JUnit都会创建一个新的测试类实例。
由于你的 GameViewModelTest
类中目前有四个测试方法,因此 GameViewModelTest
会实例化四次。每个实例都有其自己的 viewModel
属性副本。因此,测试执行的顺序无关紧要。
5. 代码覆盖率简介
代码覆盖率在确定你是否充分测试了构成应用的类、方法和代码行方面发挥着至关重要的作用。
Android Studio 为本地单元测试提供了一个代码覆盖率工具,用于跟踪单元测试覆盖的应用代码的百分比和区域。
使用 Android Studio 运行带有覆盖率的测试
要运行带有覆盖率的测试
- 右键单击项目窗格中的
GameViewModelTest.kt
文件,然后选择 使用覆盖率运行“GameViewModelTest”。
- 测试执行完成后,在右侧的覆盖率面板中,单击**展开包**选项。
- 注意如下所示的**
com.example.android.unscramble.ui
** 包。
- 双击包**
com.example.android.unscramble.ui
** 的名称,将显示GameViewModel
的覆盖率,如下所示。
分析测试报告
下图所示的报告分为两个方面
- 单元测试覆盖的方法的百分比: 在示例图中,你编写的测试到目前为止覆盖了 8 个方法中的 7 个。即总方法的 87%。
- 单元测试覆盖的代码行的百分比: 在示例图中,你编写的测试覆盖了 41 行代码中的 39 行。即 95% 的代码行。
报告表明,你编写的单元测试遗漏了代码的某些部分。要确定遗漏了哪些部分,请完成以下步骤
- 双击**GameViewModel**。
Android Studio 将显示 GameViewModel.kt
文件,并在窗口左侧添加了额外的颜色编码。亮绿色表示这些代码行已被覆盖。
当你向下滚动 GameViewModel
时,你可能会注意到几行代码用浅粉色标记。此颜色表示这些代码行未被单元测试覆盖。
提高覆盖率
要提高覆盖率,你需要编写一个测试来覆盖缺失的路径。你需要添加一个测试来断言当用户跳过一个单词时,以下内容为真
currentGameUiState.score
属性保持不变。currentGameUiState.currentWordCount
属性加 1,如下面的代码片段所示。
为了准备提高覆盖率,请将以下测试方法添加到 GameViewModelTest
类中。
@Test
fun gameViewModel_WordSkipped_ScoreUnchangedAndWordCountIncreased() {
var currentGameUiState = viewModel.uiState.value
val correctPlayerWord = getUnscrambledWord(currentGameUiState.currentScrambledWord)
viewModel.updateUserGuess(correctPlayerWord)
viewModel.checkUserGuess()
currentGameUiState = viewModel.uiState.value
val lastWordCount = currentGameUiState.currentWordCount
viewModel.skipWord()
currentGameUiState = viewModel.uiState.value
// Assert that score remains unchanged after word is skipped.
assertEquals(SCORE_AFTER_FIRST_CORRECT_ANSWER, currentGameUiState.score)
// Assert that word count is increased by 1 after word is skipped.
assertEquals(lastWordCount + 1, currentGameUiState.currentWordCount)
}
完成以下步骤以重新运行覆盖率
- 右键单击
GameViewModelTest.kt
文件,然后从菜单中选择**使用覆盖率运行“GameViewModelTest”**。 - 构建成功后,再次导航到**GameViewModel** 元素,并确认覆盖率百分比为 100%。最终的覆盖率报告如下所示。
- 导航到
GameViewModel.kt
文件并向下滚动,检查之前遗漏的路径是否现在已覆盖。
你学习了如何运行、分析和改进应用程序代码的代码覆盖率。
较高的代码覆盖率百分比是否意味着较高的应用代码质量? 否。代码覆盖率表示单元测试覆盖或执行的代码的百分比。它并不表示代码已得到验证。如果你删除单元测试代码中的所有断言并运行代码覆盖率,它仍然会显示 100% 的覆盖率。
高覆盖率并不表示测试设计正确,也不表示测试验证了应用的行为。你需要确保你编写的测试具有断言,这些断言验证了正在测试的类的行为。你也不必努力编写单元测试以获得整个应用的 100% 测试覆盖率。你应该使用 UI 测试来测试应用代码的某些部分,例如 Activity。
但是,低覆盖率意味着你的大部分代码完全没有经过测试。将代码覆盖率用作查找未被测试执行的代码部分的工具,而不是衡量代码质量的工具。
6. 获取解决方案代码
要下载完成的 codelab 的代码,可以使用以下 git 命令
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-unscramble.git $ cd basic-android-kotlin-compose-training-unscramble $ git checkout main
或者,你可以将存储库下载为 zip 文件,解压缩并将其在 Android Studio 中打开。
如果你想查看解决方案代码,请在 GitHub 上查看。
7. 结论
恭喜!你学习了如何定义测试策略并实现单元测试来测试 Unscramble 应用中的 ViewModel
和 StateFlow
。在继续构建 Android 应用时,请确保在应用功能旁边编写测试,以确认你的应用在整个开发过程中都能正常工作。
总结
- 使用
testImplementation
配置来指示依赖项适用于本地测试源代码,而不是应用代码。 - 目标是将测试分类为三种场景:成功路径、错误路径和边界情况。
- 一个好的单元测试至少具有四个特征:它们是专注的、易于理解的、确定性的和自包含的。
- 测试方法以隔离的方式执行,以避免可变测试实例状态带来的意外副作用。
- 默认情况下,在每个测试方法执行之前,JUnit都会创建一个新的测试类实例。
- 代码覆盖率在确定你是否充分测试了构成应用的类、方法和代码行方面发挥着至关重要的作用。