先聊个真实经历。去年我接手一个后台系统的版本验收,测试用例里所有必填项都写了“填空点提交,提示XX不能为空”,结果上线第三周,客服反馈“后台能创建没有手机号的企业账号”。我第一反应是“前端校验被绕过”,查下来发现接口层根本没做非空判断,数据直接从页面表单POST到服务端,服务端只校验了格式合法性,空字符串直接入库。这个问题的根源不是测试没做,而是整个必填项测试的视野太窄:只测了“页面上能不能拦住”,没测“绕过页面还能不能拦住”。
注册表单的必填项校验,看起来是测试领域最基础的场景之一,但想真正测到位,你需要理解校验的层次、触发机制、边界条件、动态表单和自动化落地方式。这篇文章把这些东西一次讲透。
1. 必填项校验的三层防线:先搞清楚你在测哪一层
必填项校验从来不只是“前端提示”那么简单。一个健壮的注册表单,至少有三层防线。测试时如果分不清自己在测哪一层,很容易出现“页面上拦住了、数据层面却裸奔”的尴尬。
1.1 前端校验:体验担当,但千万别拿它当安全边界
前端校验是用户最先感知到的一层,表现形式是HTML5原生校验、Vue/React等框架的自定义校验规则、或者纯JavaScript的onsubmit拦截。
- HTML5原生属性:input上加了required之后,浏览器会在表单提交时自动拦截空值,并弹出气泡提示。这种方案实现成本最低,但提示样式无法跨浏览器统一,而且只在表单以标准提交方式触发时才生效。
- 框架校验:国内项目里最常见的组合是Vue + Element UI的el-form或React + Ant Design的Form组件。它们通过rules配置定义必填规则,在表单校验时统一触发。优势是提示位置、样式、触发时机都可控,也是自动化测试最容易定位断言的实现方式。
- 手动JS拦截:老项目或自定义组件里常见。通过监听submit或click事件,遍历需要必填的字段,发现空值则阻止提交并给出提示。这种方式灵活性最高,但也最容易漏字段或写错条件。
我在测试前端校验时,默认带着一个怀疑:**前端校验只是为了服务用户的手误,不是为了防攻击者。**跳过前端校验的办法多到数不清——关掉浏览器JavaScript、用Postman直接调接口、修改请求参数、甚至直接在控制台改掉表单容器的校验状态。所以如果某天产品经理说“前端校验挡住了就行了”,你一定要提醒他补接口校验,这不是技术洁癖,是底线。
1.2 应用层(后端)校验:真正的业务守门员
应用层校验是服务端接收请求后、落库前进行的参数校验。它不依赖前端,直接面对HTTP请求。测试必填项时,这一层才是真正的核心。
以Java生态为例,常见实现是Bean Validation(JSR 303/380),在DTO字段上加@NotNull或@NotBlank注解:
public class RegisterRequest { @NotBlank(message = "用户名不能为空") private String username; @NotBlank(message = "手机号不能为空") private String mobile; }注意区分:
@NotNull:只校验对象是否为null,空字符串""能通过。@NotEmpty:校验null和空字符串,但纯空格" "能通过。@NotBlank:校验null、空字符串以及纯空白字符,是必填项校验里最接近“用户语义”的一个。
这一层的测试要点是:不经过页面,直接向接口发送缺参、null、空字符串、纯空格、零宽字符等不同形态的“空值”,验证服务端的拦截逻辑和响应信息是否符合预期。响应码是400还是200?提示信息是“用户名不能为空”还是“参数错误”?这些都在测试范围之内。
顺便提一个我见过很多次的缺陷:后端只做了@NotNull,结果用户在前端输入三个空格,前端trim之后提交的是空字符串,后端一看不是null直接放行,落库就成了一个“三个空格”的用户名。这不是段子,是真事。所以必填项测试在后端层面,建议至少覆盖null、空字符串、纯空格三种形态,有条件加上全角空格和Tab。
1.3 数据层约束:兜底方案
数据层的非空约束属于最后的保险。数据库字段设置了NOT NULL,即便应用层某个接口漏了校验,数据库也会抛异常,不会让脏数据落库。
但测试时要注意一个容易忽略的点:很多注册相关的表,为了业务扩展或软删除设计,某些必填字段允许NULL但业务上又要求必填。比如用户表的user_type字段是后端根据策略写的默认值,前端并不感知;如果接口层没赋值,库里就会写入NULL,功能上可能不报错,但下游统计、消息推送只要一读这个字段就是空指针。
数据层约束测试一般不作为必填项测试的主战场,但你应该在测试用例里加一条“绕过前端和接口校验,直接往表里insert空字段,观察数据库行为”,用来确认兜底机制是否存在。这不需要每次都做,核心流程回归时跑一遍就够了。
2. 必填项测试场景设计:从“空表单提交”到“组合轰炸”
很多测试新手理解“必填项测试”就是:每个字段清空,点提交,看提示。这只能覆盖最基础的一种情况。注册表单的必填项测试,我习惯把场景分成四个方向。
2.1 基础场景:空、空格与非法填充
这是必填项测试的地基,但对“空”的定义要足够细致:
- 完全不填,直接提交。
- 字段中填纯空格(半角空格、全角空格、Tab)。
- 字段中填不可见字符,比如零宽空格(U+200B)和零宽连接符。
- 输入后全部删除,触发失焦/提交。
- 输入内容后刷新页面、回退、复用浏览器自动填充,再观察字段值和校验状态。
这里有一个容易翻车的点:前端在提交前是否对输入做了trim处理。如果页面不trim,用户输入“abc ”和“abc”会被当成两个不同账号或者校验不通过导致注册失败。如果前端trim了,但后端没有,接口层面可能也会校验不一致。我建议测试用例中至少有一条是“输入内容首尾带空格”,专门验证产品在这一块的统一行为。
另外,很多表单会做“格式合法性校验”,比如手机号要满足正则、邮箱要带@。必填项测试和格式校验测试经常互相干扰。设计用例时最好拆成两个维度:
- 空值 + 合法格式 = 必填校验是否生效
- 非空 + 非法格式 = 格式校验是否生效
- 非空 + 合法格式 = 是否提交成功
三条用例分开跑,否则一旦提交被拦截,你很难判断是必填校验还是格式校验拦下来的。
2.2 触发时机与交互场景
必填提示什么时候出现,是产品交互设计的一部分,也是测试最容易漏的场景。常见触发时机有三种:
- 提交时触发:用户点注册/下一步按钮,统一校验所有必填项,所有错误一起展示。这也是大多数注册表单的做法,交互上对用户最友好,测试时注意看“多个必填项同时为空时,是否展示了所有错误提示,而不是只提示第一个字段”。
- 失焦时触发:输入框失去焦点时立即校验该字段。这种情况下要重点测试:“用户把焦点移走但没填内容”是否立刻提示;“用户填了一部分但失焦”是否提示“格式不正确”(注意,不能误报成“不能为空”)。
- 输入时实时触发:用户输入过程中实时清除错误状态或实时校验。这里要测试“先触发错误,再清空内容,错误提示会不会变成必填提示”,以及“输入合法后错误是否自动消失”。
交互层面还有一种典型场景:先填写了必填字段,然后清空再提交。很多人的用例只覆盖“从头到尾不填”,忽略了“填了再删”这个操作链路。前端框架在第一次校验通过后,通常会把该字段标记为“已验证通过”,如果用户随后删掉内容,有些组件的校验状态并不会自动重置,结果用户带着空值点了提交,竟然能成功。这个Bug在Element UI和Ant Design里我都见过,具体原因和版本有关,但测试思路是一样的:必填字段必须覆盖“从有到无”的状态迁移。
2.3 边界与组合场景
单字段测完后,要把字段组合起来当成一个系统来测:
- 全部必填项为空,一次性提交,确认所有错误提示完整显示。
- 部分必填项为空,其余填正确,确认提示只出现在空字段上,并且焦点能定位到第一个错误字段(如果有这个交互设计)。
- 必填项+非法格式混搭,比如手机号填成英文、邮箱不带@,确认错误提示不会互相覆盖。
- 必填项依赖另一个字段的值。这个在注册表单里很常见:选择“企业用户”后,“企业名称”变成必填;选择“个人用户”后,“企业名称”隐藏且非必填。这种情况要测矩阵而不是单点:A字段值切换时,B字段的必填状态、已填内容、错误提示是否都跟着正确变化。
- 键盘提交:用户输入完所有字段后按回车提交,和点击提交按钮的行为是否完全一致。很多前端校验只绑定了click事件,回车直接绕过了自定义校验,导致空表单被提交。
2.4 可直接落地的必填项用例矩阵
如果你负责的功能需要一份“拿来就能用”的必填项测试矩阵,我建议至少包含以下这些维度。表格是我常用的模板,字段名按实际表单替换即可:
| 用例类型 | 操作步骤 | 预期结果 |
|---|---|---|
| 空值提交 | 所有必填项留空,点击提交 | 每个必填字段展示“不能为空”提示,表单不提交 |
| 单一必填为空 | 仅目标字段留空,其余字段合法填写 | 仅目标字段展示提示,其余字段无异常 |
| 纯空格输入 | 目标字段输入若干个空格,点击提交 | 按产品规则:提示不能为空,或trim后视为空 |
| 首尾空格输入 | 目标字段输入“ abc ” | 按产品规则:trim后提交,或提示格式错误 |
| 填后清空 | 目标字段填写合法内容,再全部删除,点击提交 | 提示不能为空,不允许提交 |
| 失焦清空 | 目标字段输入后删除,再点击其他区域 | 失焦时按产品规则展示必填提示 |
| 异步回填清空 | 页面加载后由接口回填该字段,手动改为空后提交 | 提示不能为空,不允许提交 |
| 动态隐藏字段 | 将条件选为“隐藏该字段”的状态,提交表单 | 隐藏字段不参与必填校验,不产生错误提示 |
| 动态显示字段 | 将条件选为“显示该字段”的状态,不填提交 | 隐藏解除后立即参与必填校验,空值被拦截 |
| 多必填同时为空 | 三个以上必填项为空,提交 | 所有空字段同时展示提示,不串行覆盖 |
| 回车提交 | 键盘回车触发提交 | 与点击按钮行为一致,校验不绕过 |
| 接口层缺参 | 直接调用接口,请求体中不包含该字段 | 接口拒绝或返回明确错误信息 |
| 接口层传null | 请求体中该字段值为null | 接口返回错误,不落库 |
| 接口层传空串 | 请求体中该字段值为空字符串 | 按后端规则校验,不能直接通过 |
| 接口层传纯空格 | 请求体中该字段值为“ ” | 按后端规则校验,倾向于拒绝 |
| 绕过前端提交 | 禁用页面JavaScript后提交表单 | 前端不做拦截,但后端必须拦截 |
这张矩阵跑完,必填项校验的核心逻辑基本就锁住了。剩下的工作是根据产品规则的差异去微调预期结果。
3. 动态表单与条件必填:注册流程中最容易翻车的两个雷区
普通静态表单的必填项测试做到上一步就够了,但现在的注册流程很少有纯静态的。动态显隐、级联必填、批量导入,这三个场景是必填项测试里最容易翻车的地方,单独拿出来说。
3.1 动态显隐与级联必填
动态表单的典型场景是“注册身份”字段,选“个人”和选“企业”呈现的字段完全不同:
- 选择“个人”时,需要填写真实姓名、身份证号;
- 选择“企业”时,企业名称、营业执照号变成必填;
- 两个身份下,手机号和邮箱都是必填。
这种表单最容易出的Bug是:字段隐藏后,它的必填校验规则没有同步移除。比如用户选择“企业”后,“身份证号”的DOM被隐藏或销毁,但校验规则还挂在表单验证器里,提交时校验器遍历所有规则,发现“身份证号”为空,直接拦截,用户不知道那个字段在哪,完全无法提交。
反过来还有一种Bug:字段显示后,必填规则没挂上。用户选“企业”,企业名称输入框显示出来了,但提交时它没被校验,空着也能提交。
这类问题靠手工一个个点也能发现,但效率很低。我建议在测试用例设计时,把“切换-提交”作为组合步骤:
- 选A身份,不填任何字段,提交,记录提示列表。
- 切到B身份,不填任何字段,提交,记录提示列表。
- 反复切换A/B多次,再提交,观察校验规则是否稳定。
- 在A身份下填好字段,切到B身份,确认之前的填写内容是否保留;保留的话,后续校验是否依然正确。
级联必填的逻辑更复杂一点。举个例子:地区选择“中国大陆”时,需要填身份证号;选择“中国香港”时,身份证号非必填,但需要填“港澳通行证号”。这种依赖关系的测试,要覆盖所有级联分支,而不只是默认分支。我在实际项目中用过全组合遍历的方式:把所有身份和地区的取值列出来做笛卡尔积,每个组合跑一遍“显示哪些字段+这些字段的必填状态”,虽然用例数量会膨胀,但收益是显著的——很多隐蔽的组合缺陷,靠随便点点根本触发不了。
3.2 多步骤表单与批量导入的必填校验
多步骤注册表单(比如第一步填账号信息,第二步填身份认证,第三步填偏好设置)有一个常见问题:用户必须在第一步完成特定字段后,才能进入第二步。那必填项测试就不只是“提交时校验”,还要覆盖“点击下一步时校验”。测试的关键点是:
- 第一步有空字段,点“下一步”能不能被正确拦截?
- 被拦截后,错误提示是展示了当前步骤的字段,还是把后续步骤的隐藏字段也校验了,导致用户看到一堆“找不到在哪”的报错?
- 用户回退到第一步,清空某个必填字段,再点“下一步”,校验是否重新生效?
- 多步骤表单的数据是暂存还是提交后请求?回退后字段内容是否还在?这些内容是否会被当成“非空”而跳过校验?
还有一个批量导入的场景,做后台系统的人应该很熟:管理员通过Excel批量导入用户,有些字段在Excel里为空,但业务上必填。此时要测的不是页面上的红星星失去焦点提示,而是上传后返回的错误报告是否准确指出是哪一行哪一列缺失。我见过一个系统,导入一万条数据,有五百行缺手机号,结果前端只弹了个“导入失败,请检查模板”,用户气得直接在群里开骂。测试这种场景时,至少覆盖三种情况:
- 单行缺必填字段,错误报告里能不能定位到具体行?
- 多行缺不同字段,错误信息是聚合展示还是只显示第一条?
- 模板里删掉必填列,能否在上传前就被识别并提示“模板格式不正确”?
4. 必填项自动化测试落地:端到端和接口层怎么分工
手工测试能把功能逻辑摸清楚,但注册表单这种高频变更的模块,只靠手工回归成本很高。我通常建议用两层自动化来覆盖必填项校验。
4.1 端到端自动化:别把断言全押在“不能提交”上
端到端层面,Selenium、Cypress、Playwright都可以做,语言上选你们团队熟悉的。我自己最常用的是Python + Selenium或Playwright,配合Pytest管理用例。
写必填项用例时,最容易犯的错是“断言太粗糙”。最常见写法是:
def test_register_without_username(browser): browser.get(REGISTER_URL) # 填其他字段,不填用户名 browser.click("submit") assert "用户名不能为空" in browser.page_source这种写法的问题在于:page_source里哪怕出现一个隐藏的、存在于页面源码但不可见的提示文本,也会误判通过。更好的做法是定位到该字段的校验错误提示元素,断言它的文本和可见性:
from playwright.sync_api import Playwright, sync_playwright def test_register_without_username(): with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("https://example.com/register") # 只填非必填字段,必填用户名留空 page.fill("#email", "test@example.com") page.click("button[type=submit]") # 定位到用户名下方的错误提示,并断言可见 error_el = page.locator("#username-error") assert error_el.is_visible() assert "用户名不能为空" in error_el.inner_text() browser.close()还有一个我踩过很多次的坑:前端校验有延迟。特别是使用异步校验规则时,提交后可能还要等接口返回才能确定校验结果。如果点完提交立刻断言,页面还在loading,断言直接失败或误判通过。稳妥的做法是使用显式等待:
page.wait_for_selector("#username-error", state="visible", timeout=5000)端到端自动化不要追求覆盖所有必填项手工用例,那样脚本数量会失控,维护成本极高。我的经验是:端到端层只覆盖核心“happy path”和几个最高优先级的必填拦截场景,其他交给接口层自动化。
4.2 接口层自动化:数据驱动是必填校验的杀手锏
必填项真正的系统性回归,应该放在接口层做。原因是:
- 接口层可以精确控制请求体,构造缺参、null、空串、空格等场景,不会受前端框架限制;
- 接口层响应结构稳定,断言逻辑简单,脚本可复用率高;
- 接口层覆盖了前端绕过场景,正是前面说的“第二层防线”。
接口层自动化我推荐用Pytest + requests,配合数据驱动设计。核心思路是:把“字段名+字段值+预期响应”组织成测试数据,用例逐个执行。
import pytest import requests BASE_URL = "http://localhost:8080/api/register" @pytest.mark.parametrize( "payload, expected_message", [ ({"username": "", "mobile": "13800138000", "password": "abc123"}, "用户名不能为空"), ({"username": None, "mobile": "13800138000", "password": "abc123"}, "用户名不能为空"), ({"username": " ", "mobile": "13800138000", "password": "abc123"}, "用户名不能为空"), ({"mobile": "13800138000", "password": "abc123"}, "用户名不能为空"), ({"username": "alice", "mobile": "", "password": "abc123"}, "手机号不能为空"), ({"username": "alice", "mobile": None, "password": "abc123"}, "手机号不能为空"), ], ) def test_register_required_fields(payload, expected_message): resp = requests.post(BASE_URL, json=payload) assert resp.status_code == 400 assert expected_message in resp.json().get("message", "")这里有一个设计细节需要注意:接口返回的错误信息结构要足够稳定。如果后端校验失败时返回的是“参数校验失败”这种统一文案,不包含具体字段,那接口断言就变得很鸡肋。遇到这种情况,我建议推动开发在响应体里带上字段级别的错误详情:
{ "code": 400, "message": "请求参数校验失败", "errors": { "username": "用户名不能为空", "mobile": "手机号不能为空" } }有了这种结构化错误信息,接口自动化才能真正断言到点子上,也能为前端展示提供标准的数据来源。测试人员发现响应结构不合理时,应该主动提出来,这不只是开发的事,测试数据设计得越清晰,你的自动化脚本越省心。
另外,接口层数据驱动可以很方便地做全必填字段缺参扫描。假设注册接口有8个必填字段,你可以在脚本里遍历每个字段,逐个从完整请求体里删除,确认每个字段缺失时都有对应的错误提示,同时确认其他字段不受影响。这种全量扫描用手工做一遍要几分钟,用脚本跑就是几秒的事,特别适合每次发版前的回归。
5. 我踩过的必填项测试的坑:五类典型问题复盘
最后分享几个我实际遇到过的、比较容易栽跟头的场景。这些坑在教科书和官方文档里通常不会写,但真实项目里一个比一个常见。
5.1 全角空格与零宽字符:看起来是空的,底层不是
有一次测试发现:用户在手机号输入框里输入了三个全角空格,点击提交,前端提示“请输入正确的手机号”而不是“手机号不能为空”;用户退格删除后,前端从“格式错误”变回“不能为空”。问题不大但体验很割裂。更麻烦的是零宽字符,肉眼完全看不到,粘贴进去后字段看起来是空的,但value.length不为0,提交时既不是“为空”也不是“格式错误”,直接往后端发了脏数据。
这类问题在设计用例时就要纳入范围。我的习惯是每个必填字符串字段都加一条“全角空格/零宽字符”的用例,看产品是选择trim后当作空值处理,还是当作非法字符提示格式错误。无所谓对错,但行为必须明确且一致。
5.2 异步赋值导致校验状态“假阳性”
现代前端框架经常在页面加载后异步请求数据回填表单,比如根据URL参数自动填充推荐人账号。这就出现了一个隐蔽问题:字段确实有值了,但表单校验器的状态没有被重置。用户什么都不改,直接提交,校验器还是认为这个字段为空,注册流程卡死在“推荐人不能为空”,但界面上明明能看到推荐人账号。
复现这个Bug的手工路径是:带参数进入注册页,等待异步回填,不手动触碰该字段直接提交。如果回填的值是合法内容,正常预期应该是提交成功或进入下一步。测试时要专门覆盖这种“被程序赋值但未经过用户交互”的字段,确保校验状态跟随实际值同步更新。
5.3 多入口校验不统一
很多系统注册有多个入口:PC端网页、移动端H5、小程序、App,还有一部分是通过接口直接对接的渠道。不同入口如果各写了一套校验逻辑,很容易出现“PC端手机号必填,小程序端手机号选填”这种分裂现象。我在一个项目里就遇到过:同一个用户体系,网页端注册强制绑定手机号,App端却允许跳过后注册,结果同一个账号在不同端的体验和权限都产生差异,最后数据清洗阶段惨不忍睹。
这类问题的根因是校验规则没有收敛成单一配置,而是散落在各个端。测试能做的就是建立一份“跨端必填项对照表”,把每个端对每个必填字段的规则列出来比对,发现不一致时当成缺陷提交。如果你们后端有统一校验逻辑,建议把主要精力放在验证“后端规则是否与最严格的前端规则一致”,这是兜底的关键。
5.4 富文本编辑器与自定义组件的校验
注册表单里如果出现富文本编辑器、图片上传、城市选择器这类非输入框组件,“必填”的判断逻辑就完全不一样了。富文本编辑器输入框里可能有一段带样式的HTML,用户清空后DOM里还会残留<p><br></p>,导致校验逻辑认为“有内容”。图片上传组件的“必填”是“必须至少传一张图”,校验时机在文件上传完成之后,上传过程中提交表单,状态很难控。
测试自定义组件时,不要只看页面上的表现,还要考虑“组件暴露给表单的值到底是什么”。富文本编辑器取到的是纯文本还是HTML?图片组件取到的是文件列表还是上传后的URL?只有明确了取值逻辑,才能设计出正确的空值用例。这类用例建议在测试计划里单独分组,别和普通input字段混在一起写,否则排查问题时会很痛苦。
5.5 防重复提交缺失
这不算严格意义上的必填项校验Bug,但它跟必填项测试强相关——校验通过后,用户连点两次提交按钮,会不会注册出两个账号?有些前端框架在提交时会禁用按钮,但禁用时机是异步任务结束之后才开始点击时才生效?我用自动化工具写过连点测试,方法很简单:
button = page.locator("button[type=submit]") for _ in range(5): button.click() page.wait_for_timeout(3000) # 断言只出现一次注册成功提示 assert page.locator(".success-toast").count() == 1这虽然超出了“必填项为空”的测试范围,但它是必填校验通过后的第一道防线,我建议在注册表单的测试用例集里补上这条。防重复提交很可能不是前端代码能完全解决的,还需要后端做接口幂等性处理——这又回到第一段的结论:前端只是体验,后端才是防线。
注册表单的必填项测试,说到底拼的不是技术难度,而是视野和细致程度。把上面这些场景在项目里逐一落地,开发和你扯皮“点一下空表单不就行了吗”的时候,你就能拿出完整的矩阵让他闭嘴了。