9、文本输入测试:TextField测试、输入验证、清除文本

文本输入,是绝大多数App最基础也最频繁的操作。登录、注册、搜索、填写表单……用户每天都在和TextField打交道。作为测试,我们得确保这些输入框能正确响应输入、验证规则、以及清空内容。

我个人习惯把文本输入测试分成三个维度:输入行为验证逻辑清除与重置。今天我们就一个一个来拆解。

9.1 基础TextField测试:输入与读取

先看一个最简单的场景。假设我们有一个登录界面,用户输入用户名。测试要做的,就是模拟输入,然后验证输入框里的内容对不对。

在Compose UI测试中,我们使用performTextInput()来模拟输入。注意,不是performClick(),也不是performGesture。专门有这么一个API。

@Test
fun testTextFieldInput() {
    composeTestRule.setContent {
        var text by remember { mutableStateOf("") }
        TextField(
            value = text,
            onValueChange = { text = it },
            modifier = Modifier.testTag("usernameInput")
        )
    }

    composeTestRule
        .onNodeWithTag("usernameInput")
        .performTextInput("hello@test.com")

    composeTestRule
        .onNodeWithTag("usernameInput")
        .assertTextContains("hello@test.com")
}

这里有个小坑。我记得刚开始写Compose测试时,用了performClick()然后typeText(),结果发现输入框根本没反应。后来才搞清楚,performTextInput()才是专门为TextField设计的。它会触发onValueChange回调,而不是模拟键盘事件。

提示:如果你的TextField使用了visualTransformation(比如密码输入),assertTextContains()读取的是原始文本,不是视觉文本。这一点和UI显示不同,测试时要注意。

9.2 输入验证测试:规则与反馈

输入验证,说白了就是检查用户输入的内容是否符合要求。比如邮箱格式、密码长度、手机号位数等。

我见过很多项目把验证逻辑写在ViewModel里,UI只负责展示错误信息。这种情况下,测试要验证的是:当输入非法内容时,错误提示是否出现;当输入合法内容时,错误提示是否消失。

来看一个例子。假设我们有一个带验证的邮箱输入框:

@Composable
fun EmailTextField() {
    var email by remember { mutableStateOf("") }
    var error by remember { mutableStateOf<String?>(null) }

    TextField(
        value = email,
        onValueChange = { newValue ->
            email = newValue
            error = if (!newValue.contains("@")) "请输入有效的邮箱地址" else null
        },
        isError = error != null,
        modifier = Modifier.testTag("emailInput")
    )

    if (error != null) {
        Text(
            text = error!!,
            color = Color.Red,
            modifier = Modifier.testTag("emailError")
        )
    }
}

测试代码可以这样写:

@Test
fun testEmailValidation() {
    composeTestRule.setContent {
        EmailTextField()
    }

    // 输入非法内容
    composeTestRule
        .onNodeWithTag("emailInput")
        .performTextInput("invalid-email")

    composeTestRule
        .onNodeWithTag("emailError")
        .assertExists()
        .assertTextContains("请输入有效的邮箱地址")

    // 清空后输入合法内容
    composeTestRule
        .onNodeWithTag("emailInput")
        .performTextClearance()

    composeTestRule
        .onNodeWithTag("emailInput")
        .performTextInput("user@example.com")

    composeTestRule
        .onNodeWithTag("emailError")
        .assertDoesNotExist()
}

为什么会这样?因为performTextClearance()会清空输入框,然后onValueChange被触发,验证逻辑重新执行。输入合法内容后,错误状态被清除,错误文本节点自然就不存在了。

注意:不要用assertTextEquals()来验证错误文本。因为错误文本可能包含额外的空格或标点。用assertTextContains()更稳妥。

9.3 清除文本测试:清空与重置

清除文本,听起来简单,但实际测试中容易忽略边界情况。比如:

  • 输入框为空时,点击清除按钮会不会崩溃?
  • 清除后,验证状态是否重置?
  • 清除后,光标位置是否正常?

我曾经在一个项目中遇到过这样的bug:用户清空搜索框后,之前的搜索结果仍然显示。原因就是清除操作只清空了UI,没有触发状态更新。测试时一定要验证清除后的状态。

Compose提供了performTextClearance()方法,专门用于清空文本。它的行为是:将TextField的value设置为空字符串,并触发onValueChange回调。

@Test
fun testClearTextField() {
    composeTestRule.setContent {
        var text by remember { mutableStateOf("initial") }
        TextField(
            value = text,
            onValueChange = { text = it },
            modifier = Modifier.testTag("myTextField")
        )
    }

    // 先输入一些内容
    composeTestRule
        .onNodeWithTag("myTextField")
        .performTextInput("some content")

    // 清空
    composeTestRule
        .onNodeWithTag("myTextField")
        .performTextClearance()

    // 验证为空
    composeTestRule
        .onNodeWithTag("myTextField")
        .assertTextEquals("")
}

这里有个细节:performTextClearance()会触发onValueChange("")。如果你的onValueChange里有过滤逻辑(比如限制输入长度),清空操作也会经过这个逻辑。测试时要确保过滤逻辑不会阻止清空。

核心要点:文本输入测试的三个关键API:
  • performTextInput(text) — 模拟输入
  • performTextClearance() — 清空文本
  • assertTextContains(text) / assertTextEquals(text) — 验证内容

9.4 知识体系图

下面这张图总结了文本输入测试的核心流程和关键节点。你可以把它当作一个快速参考。

文本输入测试知识体系 输入行为测试 performTextInput() 验证逻辑测试 assertTextContains() 清除文本测试 performTextClearance() 模拟键盘输入 触发 onValueChange 非法输入 → 错误提示 合法输入 → 错误消失 清空内容 重置验证状态 测试核心:输入 → 验证 → 清除 → 再验证 确保每个状态转换都正确,边界情况不遗漏

9.5 常见陷阱与避坑指南

做文本输入测试时,有几个坑我踩过,分享给你:

  • 不要用typeText()代替performTextInput()typeText()模拟的是键盘输入,但Compose的TextField可能不会正确响应。用performTextInput()更可靠。
  • 验证错误文本时,用assertTextContains()而不是assertTextEquals():错误文本可能包含额外的空格或标点,精确匹配容易失败。
  • 清除文本后,别忘了验证验证状态:清空输入框后,错误提示应该消失。如果不清除,用户会看到残留的错误信息。
  • 注意visualTransformation的影响:密码输入框的assertTextContains()读取的是原始文本,不是显示出来的星号。这一点和UI表现不同。
避坑指南:我曾经在测试密码输入框时,用assertTextContains("****")去验证,结果测试一直失败。后来才发现,assertTextContains()读取的是原始文本,不是视觉文本。正确的做法是验证原始密码字符串。

9.6 小结

文本输入测试,说白了就是三件事:输入、验证、清除。每个环节都有对应的API和注意事项。掌握了这些,你就能写出健壮的输入测试用例。

嗯,这里要注意一点:测试不是一次性的。每次修改输入逻辑或验证规则后,都要重新跑一遍测试。自动化测试的价值就在这里——它能帮你快速发现回归问题。

好了,文本输入测试就讲到这里。下一章我们会聊更复杂的场景——列表与滚动测试。到时候见。


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