第三章 第一个 Compose UI 测试用例:创建、编排与验证
在本章中,我们将动手编写第一个 Compose UI 测试用例。你不仅会学会如何搭建测试类,还会深入理解 createComposeRule 的内部原理、测试生命周期,以及点击验证的底层机制。通过本章的学习,你将建立起一套可复用的测试编写流程。
3.1 测试用例的核心原理
Compose 测试建立在 Compose 测试规则 (Test Rule) 之上。该规则负责管理 Compose 的合成 (Composition) 生命周期,并提供一个测试上下文,让测试代码可以直接与 UI 控件交互。
当你调用 createComposeRule() 时,系统会:
- 创建一个独立的测试宿主 Activity(类似一个空壳 Activity);
- 在该 Activity 上初始化 Compose 环境;
- 挂载待测试的 Composable 函数;
- 等待布局帧渲染完成,确保 UI 树可交互。
整个流程形成了一个闭环:创建环境 → 挂载内容 → 查找元素 → 执行动作 → 断言状态。下面这张流程图清晰地展示了这个核心流程。
3.2 实现流程:编写你的第一个测试
3.2.1 创建测试类
测试类必须位于 src/androidTest 目录下,包名通常与待测试组件的包名一致或接近(例如 com.example.app)。
LoginButtonTest、ProfileCardTest。这样可以保持测试与源文件间的可追溯性。
3.2.2 声明 Compose 测试规则
使用 @get:Rule 注解声明一个 createComposeRule() 实例。这是框架与测试之间的桥梁。
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test
class ButtonClickTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testButtonClick_changesText() {
// 测试逻辑将在此处展开
}
}
3.2.3 挂载 Composable 内容
在测试方法内,通过 composeTestRule.setContent { ... } 将待测试的 UI 代码挂载到测试宿主中。这一步对应流程图中的“挂载 UI”。
@Test
fun testButtonClick_changesText() {
composeTestRule.setContent {
// 直接编写或引用待测试的 Composable
MyClickableButton()
}
}
假设我们有一个简单的按钮组件 MyClickableButton:
@Composable
fun MyClickableButton() {
var text by remember { mutableStateOf("点击我") }
Button(onClick = { text = "已点击" }) {
Text(text = text)
}
}
3.2.4 查找元素并执行点击
Compose 测试使用语义树(Semantics Tree)来查找 UI 节点。常用的查找函数包括:
onNodeWithText("点击我")—— 通过显示文本查找;onNodeWithTag("my_button")—— 通过Modifier.testTag()查找;onNodeWithContentDescription("描述")—— 通过无障碍描述查找。
查找到节点后,调用 .performClick() 模拟用户点击:
composeTestRule.onNodeWithText("点击我")
.performClick()
3.2.5 编写点击验证
点击后,使用 .assertIsDisplayed()、.assertTextEquals() 或 .assert(hasText("已点击")) 验证 UI 状态是否符合预期:
composeTestRule.onNodeWithText("已点击")
.assertIsDisplayed()
// 或者使用更精确的文本匹配
composeTestRule.onNodeWithText("已点击")
.assert(hasText("已点击"))
3.2.6 完整的测试用例
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test
class ButtonClickTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testButtonClick_changesText() {
// Arrange: 挂载测试组件
composeTestRule.setContent {
var text by remember { mutableStateOf("点击我") }
Button(onClick = { text = "已点击" }) {
Text(text = text)
}
}
// Act: 查找按钮并执行点击
composeTestRule.onNodeWithText("点击我")
.performClick()
// Assert: 验证文本变更
composeTestRule.onNodeWithText("已点击")
.assertIsDisplayed()
}
}
3.3 运行测试
测试可以在 Android Studio 内直接运行,也可以通过 Gradle 命令行执行:
# 运行单个测试类
./gradlew :app:connectedDebugAndroidTest --tests "com.example.app.ButtonClickTest"
# 运行所有 Android 测试
./gradlew :app:connectedDebugAndroidTest
运行时,设备或模拟器上会自动打开测试宿主 Activity,对应的 UI 会一闪而过(通常不可见),但测试报告会立即在 Run 窗口中输出。
3.4 调试与常见错误
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
| @get:Rule 报错 “Cannot resolve symbol” | 缺少 compose-ui-test 依赖 |
检查 module 级 build.gradle.kts 是否包含 androidTestImplementation("androidx.compose.ui:ui-test-junit4") |
| onNodeWithText 找不到节点,抛出 AssertionError |
文本与语义树中的字符串不匹配,或组件未渲染 | 使用 composeTestRule.onAllNodesWithText().fetchSemanticsNodes() 打印所有节点 |
| performClick 后断言失败 | 异步状态更新尚未完成;或 Compose 重组未触发 | Compose 测试框架应自动等待;若未生效,尝试 composeTestRule.waitForIdle() |
| 测试类未被识别(No tests found) | 测试方法缺少 @Test 注解,或测试类未继承 JUnit4 运行器 |
确保使用 @RunWith(AndroidJUnit4::class) 或 Gradle 配置 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" |
3.5 性能与稳定性建议
- 避免在 setContent 中执行耗时操作:挂载应该是轻量级的,只包含 UI 定义;数据准备应该在 setContent 之外完成。
- 善用 TestTag:为可交互组件添加稳定的
Modifier.testTag(),避免因文案变更导致测试失效。 - 控制测试粒度:每个测试只验证一个行为。不要在一次测试中连续点击多个按钮,否则错误定位会变得困难。
你已经亲手搭建了完整的 Compose UI 测试管道——从测试规则、UI 挂载到节点交互和断言。这个模式将贯穿本课程后续所有章节,请确保你完全理解了
createComposeRule 与 setContent 的作用。接下来,我们将深入探讨更丰富的节点匹配策略。