25、表单验证测试:输入校验测试、错误提示测试、提交逻辑测试
表单验证,说白了就是给用户输入加一道「安检门」。用户填了什么?填得对不对?错了怎么提醒?提交时能不能拦住?这些都得靠测试来兜底。
我在项目中遇到过好几次,表单看起来跑得挺好,结果上线第一天就被用户吐槽:「我邮箱少打了个点,它居然让我提交成功了!」——嗯,从那以后,我对表单验证测试就格外上心。
25.1 输入校验测试:把边界值都试一遍
输入校验测试,核心就一句话:合法数据放行,非法数据拦截。但「非法」的定义,往往藏在业务细节里。
我个人习惯把校验规则分成三类:
- 必填校验:空字符串、纯空格、null
- 格式校验:邮箱、手机号、密码强度、URL
- 范围校验:数字区间、字符长度、枚举值
举个例子,一个注册表单的邮箱输入框,测试用例可以这样设计:
@Test
fun emailValidation_emptyInput_showsError() {
composeTestRule.setContent {
SignUpForm()
}
// 不输入任何内容,直接点击提交
composeTestRule.onNodeWithText("提交").performClick()
composeTestRule.onNodeWithText("邮箱不能为空").assertExists()
}
@Test
fun emailValidation_invalidFormat_showsError() {
composeTestRule.setContent {
SignUpForm()
}
composeTestRule.onNodeWithTag("email_input").performTextInput("abc@")
composeTestRule.onNodeWithText("提交").performClick()
composeTestRule.onNodeWithText("请输入有效的邮箱地址").assertExists()
}
@Test
fun emailValidation_validFormat_passes() {
composeTestRule.setContent {
SignUpForm()
}
composeTestRule.onNodeWithTag("email_input").performTextInput("test@example.com")
composeTestRule.onNodeWithText("提交").performClick()
composeTestRule.onNodeWithText("请输入有效的邮箱地址").assertDoesNotExist()
}
小技巧:用
performTextInput 模拟用户逐字输入,比 setText 更贴近真实行为。我习惯给每个输入框加 testTag,这样定位更稳,不会因为文案改动就崩掉。
25.2 错误提示测试:不仅要出现,还要「对」
错误提示测试,很多人只测「有没有出现」,但忽略了「出现的位置对不对」「文案对不对」「消失的时机对不对」。
我曾经踩过一个坑:密码强度校验,弱密码时提示「密码太弱」,但用户改成强密码后,提示还在——因为忘记在状态变化时清除错误信息了。
所以,错误提示测试至少覆盖这三点:
- 出现时机:是输入时实时校验,还是提交时统一校验?
- 文案正确性:不同错误类型对应不同文案,别张冠李戴。
- 消失逻辑:用户修正后,错误提示是否自动消失?
@Test
fun passwordError_disappearsAfterCorrection() {
composeTestRule.setContent {
SignUpForm()
}
// 先输入弱密码
composeTestRule.onNodeWithTag("password_input").performTextInput("123")
composeTestRule.onNodeWithText("提交").performClick()
composeTestRule.onNodeWithText("密码至少8位").assertExists()
// 修正为强密码
composeTestRule.onNodeWithTag("password_input").performTextClearance()
composeTestRule.onNodeWithTag("password_input").performTextInput("StrongPass1!")
composeTestRule.onNodeWithText("密码至少8位").assertDoesNotExist()
}
注意:
performTextClearance() 会清空输入框,但不会触发 IME 操作。如果你的校验逻辑依赖 onValueChange 中的 TextFieldValue 变化,记得确保清空后也触发了校验。
25.3 提交逻辑测试:拦得住,也放得对
提交逻辑测试,是表单验证的「最后一关」。它要验证两件事:
- 校验失败时,提交被拦截——按钮不可点,或者点击后不触发网络请求。
- 校验通过时,提交正常执行——调用 ViewModel 的提交方法,或者跳转页面。
我个人习惯用 ComposeTestRule 配合 fake ViewModel 来测。这样不依赖真实网络,跑得快,也稳。
class FakeSignUpViewModel : SignUpViewModel() {
var submitCalled = false
override fun submit() {
submitCalled = true
}
}
@Test
fun submit_buttonDisabled_whenFormInvalid() {
val viewModel = FakeSignUpViewModel()
composeTestRule.setContent {
SignUpForm(viewModel = viewModel)
}
// 表单初始状态,所有字段为空
composeTestRule.onNodeWithText("提交").assertIsNotEnabled()
}
@Test
fun submit_triggersViewModel_whenFormValid() {
val viewModel = FakeSignUpViewModel()
composeTestRule.setContent {
SignUpForm(viewModel = viewModel)
}
// 填入所有合法数据
composeTestRule.onNodeWithTag("email_input").performTextInput("a@b.com")
composeTestRule.onNodeWithTag("password_input").performTextInput("Pass1234!")
composeTestRule.onNodeWithText("提交").performClick()
assertTrue(viewModel.submitCalled)
}
核心思路:把 ViewModel 抽象成接口或 open 类,测试时注入 fake 实现。这样你就能精准控制「提交是否被调用」,而不需要关心网络层。
25.4 知识体系总览
下面这张图,把表单验证测试的三大块串起来了。你写测试时,可以照着这个结构来组织用例:
25.5 实战建议:测试用例的组织方式
你想想看,一个表单可能有十几个字段,每个字段又有好几种校验规则。如果全写在一个测试类里,很快就变成一团乱麻。
我推荐按字段分组,每个字段一个嵌套测试类:
@Nested
@DisplayName("邮箱字段校验")
inner class EmailValidation {
@Test fun empty() { ... }
@Test fun missingAt() { ... }
@Test fun missingDomain() { ... }
@Test fun valid() { ... }
}
@Nested
@DisplayName("密码字段校验")
inner class PasswordValidation {
@Test fun tooShort() { ... }
@Test fun missingNumber() { ... }
@Test fun valid() { ... }
}
这样跑测试时,IDE 会按分组折叠显示,一眼就能看出哪个字段的校验出了问题。我在项目里一直这么用,团队反馈说排查效率高了不少。
额外提醒:别忘了测「边界值」。比如密码长度要求 8-20 位,那 7 位、8 位、20 位、21 位都得测。很多线上 bug 就出在边界上。
表单验证测试,说白了就是给用户输入上了一道保险。输入校验测边界,错误提示测时机和文案,提交逻辑测拦截与放行。这三块都覆盖到了,表单的质量基本就稳了。
公众号:蓝海资料掘金营,微信deep3321