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 确保可见,再 performClickperformTextInput 执行交互,最后用 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 知识体系图

Compose UI 测试核心操作 performClick performTextInput performScrollTo 模拟按钮点击 模拟文字输入 滚动到目标项 组合使用 执行顺序:先滚动 → 再操作 → 最后断言 确保目标可见 → 执行交互 → 验证结果

这张图把三个操作的关系理清楚了。你想想看,实际测试中很少单独用某个操作,基本都是组合使用。先 performScrollTo 确保目标可见,再 performClickperformTextInput 执行交互,最后用断言验证结果。这个流程,说白了就是 UI 测试的「黄金三角」。


公众号:蓝海资料掘金营,微信deep3321