第4章 交互动作模拟:performClick、performTextInput、performGesture 等 API 使用
4.1 为什么需要交互动作模拟
在 Compose UI 测试中,仅仅检查布局树或文本内容是不够的。真实用户场景涉及点击、输入、滑动等操作。交互动作模拟 API 允许测试脚本以编程方式触发这些事件,从而验证组件的可交互行为(如按钮响应、输入框更新、列表滚动等)。
Compose Test 框架基于语义树(Semantics Tree)工作,所有模拟操作最终作用于 SemanticsNodeInteraction 对象,框架自动将语义动作分派到对应组件。这避免了直接操作底层输入系统,使测试与设备解耦。
4.2 核心 API 一览
| API | 作用 | 适用场景 |
|---|---|---|
performClick() |
模拟一次点击 | 按钮、单选、复选框、列表项等可点击节点 |
performTextInput() |
模拟文本输入(追加或替换) | TextField、OutlinedTextField 等文本输入节点 |
performGesture { … } |
自定义手势操作(滑动、长按、多指) | 列表滚动、拖动滑块、缩放等复杂手势 |
performTouchInput { … } |
底层触摸事件序列 | 需要精确坐标或手势合成的场景 |
4.3 实现流程
- 构建待测 UI:在测试方法中使用
createComposeRule()并设置 content。 - 定位语义节点:通过
onNodeWithText()、onNodeWithTag()等获取SemanticsNodeInteraction。 - 执行模拟动作:调用相应的
perform*方法。 - 等待 UI 空闲:使用
composeTestRule.waitForIdle()确保异步操作完成。 - 验证结果:再次定位节点并断言状态变化。
关键说明:所有
perform* 方法都会自动等待预期动作可见并启用;若节点无效或不可交互,将抛出 AssertionError。
4.4 代码示例
4.4.1 performClick
class ClickTest {
@get:Rule
val rule = createComposeRule()
@Test
fun clickButton_increasesCounter() {
rule.setContent {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
rule.onNodeWithText("Count: 0").performClick()
rule.waitForIdle()
rule.onNodeWithText("Count: 1").assertExists()
}
}
4.4.2 performTextInput
class TextInputTest {
@get:Rule
val rule = createComposeRule()
@Test
fun inputText_updatesTextField() {
rule.setContent {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })
}
rule.onNodeWithTag("myTextField").performTextInput("Hello")
rule.waitForIdle()
rule.onNodeWithText("Hello").assertExists()
}
}
4.4.3 performGesture – 滑动
class SwipeTest {
@get:Rule
val rule = createComposeRule()
@Test
fun swipeLazyColumn_scrollToEnd() {
rule.setContent {
LazyColumn {
items(50) { index -> Text("Item $index") }
}
}
rule.onNodeWithText("Item 0").performGesture {
swipeUp()
}
rule.waitForIdle()
rule.onNodeWithText("Item 49").assertIsDisplayed()
}
}
4.5 调试与常见错误
- 节点不可见或不可交互:使用
rule.onAllNodes()+.printToLog()打印当前语义树,检查节点是否被遮罩或禁用。 - performTextInput 无效:确保组件类型为
EditableText子类;若为只读文本,请改用assertTextEquals。 - 手势方向错误:
swipeUp()默认在节点上触发向上滑动;注意节点本身必须支持手势事件(如LazyColumn的滚动容器)。 - 异步后遗症:在
perform*后一定要调用rule.waitForIdle(),否则后续断言可能因动画未完成而失败。
常见陷阱:
performTextInput 在受控输入框(使用 TextField 且 onValueChange 正确)上工作良好;但若 onValueChange 未更新状态,输入将不生效。请在测试前单独验证组件的状态更新逻辑。
4.6 性能与稳定性优化
- 使用 waitForIdle() 代替固定延时:避免 Thread.sleep,依赖框架的 idle 检测。
- 合并手势:多个连续操作(如点击输入框 → 输入 → 点击按钮)可流水线执行,减少上下文重建。
- 避免过度使用 performTouchInput:尽量使用高层语义 API(performClick/performTextInput),它们更稳定且不依赖坐标。
- 提示:对于大量列表项滚动,考虑使用
performGesture { swipeBy(SwipeDirection.Down, 0.5f) }相对坐标滑动,减少 UI 重绘次数。
4.7 交互动作模拟执行流程框架
最佳实践:编写测试时优先使用
performGesture 实现自定义手势,如 performGesture { swipeDown() }。它比 performTouchInput 更简洁且自动处理边界。若需要坐标级精确控制(如画布绘图),再降级使用 performTouchInput。