5、执行操作:performClick、performTextInput、performScrollTo
写UI测试,说白了就是模拟用户的操作。用户点按钮,我们就点按钮;用户输入文字,我们就输入文字;用户滑动列表,我们就滑动列表。Compose 测试框架提供了三个最核心的操作方法,今天我们就来把它们彻底搞明白。
5.1 performClick:模拟点击
这是最常用的操作,没有之一。我刚开始写 Compose 测试时,第一个学会的就是它。
@Test
fun testButtonClick() {
composeTestRule.setContent {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击次数: $count")
}
}
// 找到按钮并点击
composeTestRule
.onNodeWithText("点击次数: 0")
.performClick()
// 验证点击后文字变化
composeTestRule
.onNodeWithText("点击次数: 1")
.assertExists()
}
嗯,这里有个细节要注意。performClick() 返回的是 SemanticsNodeInteraction,所以你可以链式调用。但我个人习惯把点击和断言分开写,这样可读性更好。
小技巧: 如果按钮有多个匹配项,记得先用
assertIsDisplayed() 确认唯一性,再执行点击。我在项目中遇到过因为两个按钮文字相同导致点击错位的坑。
5.2 performTextInput:模拟输入
输入框的测试,核心就是 performTextInput。它会把传入的字符串逐字输入到目标组件中。
@Test
fun testTextFieldInput() {
composeTestRule.setContent {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("用户名") }
)
}
// 找到输入框并输入文字
composeTestRule
.onNodeWithText("用户名")
.performTextInput("admin")
// 验证输入结果
composeTestRule
.onNodeWithText("admin")
.assertExists()
}
你想想看,如果用户输入的是密码,我们怎么验证?其实可以用 assertTextContains 来检查部分匹配,或者用 assertTextEquals 做精确匹配。
注意:
performTextInput 会触发 onValueChange 回调,但不会触发 IME 动作(如回车键)。如果需要模拟回车,得用 performKeyPress。
5.3 performScrollTo:模拟滚动
列表测试中,performScrollTo 是神器。它会把目标项滚动到可见区域,然后你才能对它进行点击或断言。
@Test
fun testScrollToItem() {
composeTestRule.setContent {
LazyColumn {
items(100) { index ->
Text("第 $index 项")
}
}
}
// 滚动到第50项
composeTestRule
.onNodeWithText("第 50 项")
.performScrollTo()
// 现在可以点击它了
composeTestRule
.onNodeWithText("第 50 项")
.assertIsDisplayed()
}
我曾经踩过一个坑:如果列表项还没渲染出来,performScrollTo 会直接抛异常。解决方案是先设置一个超时等待,或者用 waitUntilExists 确保节点存在。
核心要点: 这三个操作方法的执行顺序很重要。先
performScrollTo 确保可见,再 performClick 或 performTextInput 执行交互,最后用 assertExists 验证结果。
5.4 组合使用:一个完整的测试场景
我们来看一个综合例子,把三个操作串起来:
@Test
fun testCompleteFlow() {
composeTestRule.setContent {
var items = remember { mutableStateListOf<String>() }
var input by remember { mutableStateOf("") }
Column {
TextField(
value = input,
onValueChange = { input = it },
label = { Text("添加内容") }
)
Button(onClick = {
if (input.isNotBlank()) {
items.add(input)
input = ""
}
}) {
Text("添加")
}
LazyColumn {
items(items) { item ->
Text(item)
}
}
}
}
// 1. 输入文字
composeTestRule
.onNodeWithText("添加内容")
.performTextInput("测试数据")
// 2. 点击按钮
composeTestRule
.onNodeWithText("添加")
.performClick()
// 3. 滚动到新添加的项
composeTestRule
.onNodeWithText("测试数据")
.performScrollTo()
// 4. 验证显示
composeTestRule
.onNodeWithText("测试数据")
.assertIsDisplayed()
}
这个流程覆盖了输入、点击、滚动、断言四个环节。说白了,这就是 UI 测试的「四步走」套路。
5.5 常见问题与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 点击无响应 | 组件被遮挡或不可点击 | 先 assertIsEnabled() 再点击 |
| 输入内容不对 | 输入框有过滤逻辑 | 检查 onValueChange 实现 |
| 滚动找不到项 | 列表未完全渲染 | 增加超时或使用 waitUntilExists |
| 多个匹配项 | 语义节点不唯一 | 使用 onFirst() 或添加 testTag |
我的经验: 写测试时,尽量给关键组件加上
testTag。比如 Modifier.testTag("submit_button"),这样查找时用 onNodeWithTag("submit_button"),既快又准,不会因为文字变化而失效。
5.6 知识体系图
这张图把三个操作的关系理清楚了。你想想看,实际测试中很少单独用某个操作,基本都是组合使用。先 performScrollTo 确保目标可见,再 performClick 或 performTextInput 执行交互,最后用断言验证结果。这个流程,说白了就是 UI 测试的「黄金三角」。