1. 准备工作
在此 Codelab 中,您将学习如何编写单元测试来测试 ViewModel
组件。您将为 Unscramble 游戏应用添加单元测试。Unscramble 应用是一款有趣的文字游戏,玩家需要猜测一个乱序词,猜对将获得积分。下图显示了应用的预览:
在编写自动化测试 Codelab 中,您了解了什么是自动化测试以及自动化测试为何如此重要。您还学习了如何实现单元测试。
您已完成以下内容的学习:
- 自动化测试是一段代码,用于验证另一段代码的准确性。
- 测试是应用开发流程的一个重要环节。通过持续对应用运行测试,您可以在公开发布应用之前验证其功能行为和易用性。
- 借助单元测试,您可以测试函数、类和属性。
- 本地单元测试在您的工作站上执行,这意味着它们可以在开发环境中运行,不需要使用 Android 设备或模拟器。换句话说,本地测试是在您的计算机上运行的。
在继续之前,请确保您已完成以下 Codelab:编写自动化测试和 Compose 中的 ViewModel 和状态。
前提条件
- 了解 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
文件的依赖项代码块中指定依赖项配置(例如 implementation
或 testImplementation
)。每种依赖项配置都向 Gradle 提供了有关如何使用相应依赖项的不同说明。
如需添加依赖项,请执行以下操作:
- 打开
app
模块的build.gradle.kts
文件,该文件位于 Project 窗格的app
目录中。
- 在文件内,向下滚动,直到找到
dependencies{}
代码块。使用testImplementation
配置为junit
添加依赖项。
plugins {
...
}
android {
...
}
dependencies {
...
testImplementation("junit:junit:4.13.2")
}
- 在 build.gradle.kts 文件顶部的通知栏中,点击 Sync Now,这将完成导入和构建,如以下屏幕截图所示:
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 库版本号。
- BoM 是使用
implementation platform("androidx.compose:compose-bom:2023.06.01")
导入的
这是因为 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 应用中的成功路径示例如下:当用户输入正确的单词并点击 Submit 按钮时,系统会正确更新得分、单词数和乱序词。
- 错误路径:错误路径测试侧重于测试负向流的功能,即检查应用如何响应错误情况或无效用户输入。确定所有可能的错误流是一项极具挑战性的任务,因为如果未实现预期行为,则会有许多可能的结果。
一项一般性建议是列出所有可能的错误路径,针对这些错误编写测试,并随着您发现不同的场景而不断改进单元测试。
Unscramble 应用中的错误路径示例如下:当用户输入错误的单词并点击 Submit 按钮时,这会导致系统显示错误消息,并且不会更新得分和单词数。
- 边界情况:边界情况侧重于测试应用中的边界条件。在 Unscramble 应用中,边界会检查应用加载时的界面状态,以及用户达到单词数量上限后的界面状态。
围绕这些类别创建测试场景,可以作为测试计划的准则。
创建测试
有效的单元测试通常具有以下四个属性:
- 有针对性:测试应侧重于某个单元,例如一段代码,通常是某个类或方法。测试应有针对性并侧重于验证单段代码的正确性,而不是同时验证多段代码。
- 易于理解:当您阅读代码时,代码应当简单且易于理解。开发者应当能够立即一目了然地了解测试背后的意图。
- 确定性:应保持一致的通过或失败结果。如果您运行测试多次且没有更改任何代码,测试应得到相同的结果。测试应避免不可靠性,也就是在未修改代码的情况下,不得在一个实例中测试失败,而在另一个实例中测试通过。
- 独立性:测试不需要任何人为互动或设置,即可独立运行。
成功路径
如需为成功路径编写单元测试,您需要做出以下断言 - 在 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
中的乱序词中获取字母顺序正确的相应单词。为此,请先获取当前的游戏界面状态。
- 在函数主体中,创建一个
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
- 运行测试并确认测试通过。
边界情况
如需测试界面的初始状态,您需要为 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
- 运行测试并确认测试通过。
另一种边界情况是,在用户猜测所有单词后测试界面状态。您需要做出以下断言:当用户猜对所有单词时,以下情况是正确的。
- 得分是最新的;
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 运行覆盖率测试
如需运行覆盖率测试,请执行以下操作:
- 右键点击“Project”窗格中的
GameViewModelTest.kt
文件,然后选择 Run 'GameViewModelTest' with Coverage。
- 测试执行完成后,点击右侧“Coverage”面板中的 Flatten Packages 选项。
- 请注意
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
文件,然后从菜单中选择 Run ‘GameViewModelTest' with Coverage。 - 构建成功后,再次找到 GameViewModel 元素,并确认覆盖率百分比为 100%。最终覆盖率报告如下图所示。
- 找到
GameViewModel.kt
文件并向下滚动,看看之前遗漏的路径是否已被覆盖。
您学习了如何运行、分析和改进应用代码的代码覆盖率。
代码覆盖率较高是否意味着应用代码的质量较高?否。代码覆盖率表示单元测试所覆盖或执行的代码占全部代码的百分比。这并不表示该代码已通过验证。如果您从单元测试代码中移除所有断言并运行代码覆盖率,它仍会显示 100% 覆盖率。
覆盖率较高并不表示测试设计正确,也不表示测试可以验证应用的行为。您需要确保在所编写的测试中包含一些断言,并通过这些断言来验证待测试类的行为。您也不必费力编写单元测试,让整个应用实现 100% 的测试覆盖率,而应改用界面测试来测试应用代码的某些部分(例如 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 都会创建测试类的一个新实例。
- 代码覆盖率对于确定您是否对组成应用的类、方法和代码行进行了充分测试至关重要。