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回调,而不是模拟键盘事件。
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里有过滤逻辑(比如限制输入长度),清空操作也会经过这个逻辑。测试时要确保过滤逻辑不会阻止清空。
performTextInput(text)— 模拟输入performTextClearance()— 清空文本assertTextContains(text)/assertTextEquals(text)— 验证内容
9.4 知识体系图
下面这张图总结了文本输入测试的核心流程和关键节点。你可以把它当作一个快速参考。
9.5 常见陷阱与避坑指南
做文本输入测试时,有几个坑我踩过,分享给你:
- 不要用
typeText()代替performTextInput():typeText()模拟的是键盘输入,但Compose的TextField可能不会正确响应。用performTextInput()更可靠。 - 验证错误文本时,用
assertTextContains()而不是assertTextEquals():错误文本可能包含额外的空格或标点,精确匹配容易失败。 - 清除文本后,别忘了验证验证状态:清空输入框后,错误提示应该消失。如果不清除,用户会看到残留的错误信息。
- 注意
visualTransformation的影响:密码输入框的assertTextContains()读取的是原始文本,不是显示出来的星号。这一点和UI表现不同。
assertTextContains("****")去验证,结果测试一直失败。后来才发现,assertTextContains()读取的是原始文本,不是视觉文本。正确的做法是验证原始密码字符串。
9.6 小结
文本输入测试,说白了就是三件事:输入、验证、清除。每个环节都有对应的API和注意事项。掌握了这些,你就能写出健壮的输入测试用例。
嗯,这里要注意一点:测试不是一次性的。每次修改输入逻辑或验证规则后,都要重新跑一遍测试。自动化测试的价值就在这里——它能帮你快速发现回归问题。
好了,文本输入测试就讲到这里。下一章我们会聊更复杂的场景——列表与滚动测试。到时候见。
公众号:蓝海资料掘金营,微信deep3321