第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 实现流程

  1. 构建待测 UI:在测试方法中使用 createComposeRule() 并设置 content。
  2. 定位语义节点:通过 onNodeWithText()、onNodeWithTag() 等获取 SemanticsNodeInteraction。
  3. 执行模拟动作:调用相应的 perform* 方法。
  4. 等待 UI 空闲:使用 composeTestRule.waitForIdle() 确保异步操作完成。
  5. 验证结果:再次定位节点并断言状态变化。
关键说明:所有 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 交互动作模拟执行流程框架

交互动作模拟执行流程 测试启动 创建ComposeTestRule 定位语义节点 执行perform*动作 等待UI空闲 验证结果 动作示例 · performClick() · performTextInput() · performGesture { … } · performTouchInput { … }
最佳实践:编写测试时优先使用 performGesture 实现自定义手势,如 performGesture { swipeDown() }。它比 performTouchInput 更简洁且自动处理边界。若需要坐标级精确控制(如画布绘图),再降级使用 performTouchInput。