原文用“功能、界面、性能、兼容性”等维度提醒自己不要遗漏测试方向,这个用途还在。但“测试正常登录”不是一条可以执行的用例,“万能模型”也不会自动告诉我什么结果才正确。
我想把这篇补成一个完整的小过程:先固定需求,再选输入,写出预期,执行后检查。否则表格再长,也可能只是在重复“应该正常”。
1. 先问需求,别先列上百条输入
以下是自建的本地输入校验模型,不连接网站,不验证账号密码是否匹配,不签发 Token,也不模拟真实登录服务。只讨论这几条明确规则:
| 编号 | 规则 |
|---|---|
| R1 | 服务不可用时,返回 SERVICE_UNAVAILABLE,优先于其他检查 |
| R2 | 服务可用但账号停用时,返回 ACCOUNT_DISABLED |
| R3 | 前两项通过后,密码输入必须是字符串,长度6~12,含边界 |
| R4 | 本演示只接受ASCII英文字母和数字,不裁剪空格、不做字符转换 |
R3、R4 不通过返回 INVALID_FORMAT,全部通过返回 FORMAT_OK。FORMAT_OK 只表示格式通过,不表示认证成功。6~12 位也是为了延续原文的练习示例,不是给真实系统推荐的密码策略;真实策略要另审长度上限、字符支持与安全需求。
如果需求没有说服务不可用与账号停用谁先提示,测试不能自己拍脑袋判定正确输出。先记录疑问,确认优先级,再写预期。
2. 等价类和边界不是一串随意挑的数字
在服务可用、账号启用的前提下,长度分成小于6、6~12、大于12三类。选8位是合法类的一个代表,但它不能代替端点测试。
| 用例 | 输入长度 | 预期 | 覆盖点 |
|---|---|---|---|
| L1 | 5 | INVALID_FORMAT | 下界外侧 |
| L2 | 6 | FORMAT_OK | 下界本身 |
| L3 | 7 | FORMAT_OK | 下界内侧 |
| L4 | 11 | FORMAT_OK | 上界内侧 |
| L5 | 12 | FORMAT_OK | 上界本身 |
| L6 | 13 | INVALID_FORMAT | 上界外侧 |
这里是离散的整数长度,所以可以使用相邻的三个点检查每个边界。不能把“永远测 n±1”搬到时间、浮点金额、Unicode 字符等任意输入域。
长度测试全部使用 ASCII 字母,以免同时引入字符规则导致归因混乱。再另外测空格、标点、非ASCII字符和非字符串。无效输入是否属于同一类,取决于处理规则,不是都给它贴一个“错误数据”标签就结束。
3. 判定表把多条件与优先级写出来
先使用合法输入 abc123,隔离格式因素。
| 账号启用 | 服务可用 | 预期 |
|---|---|---|
| true | true | FORMAT_OK |
| false | true | ACCOUNT_DISABLED |
| true | false | SERVICE_UNAVAILABLE |
| false | false | SERVICE_UNAVAILABLE |
最后一行正是优先级测试。如果把两个判断互换,只测“账号停用”与“服务不可用”各自单独出现,是发现不了这个变化的。
判定表用于列条件组合与动作;因果图可以辅助整理条件关系,但不是判定表的另一个名字。组合很多时,还要标记不可能状态、约束和风险,不能无条件要求所有场景都暴力穷举。
4. 把表中的预期变成可以运行的断言
保存为 input-check.cjs,使用 Node.js 执行。所有预期都写在数据表里,不让校验函数自己给自己生成正确答案。
const assert = require('node:assert/strict'); function assessInput(password, enabled, online) { if (!online) return 'SERVICE_UNAVAILABLE'; if (!enabled) return 'ACCOUNT_DISABLED'; if (typeof password !== 'string' || password.length < 6 || password.length > 12 || /[^A-Za-z0-9]/.test(password)) { return 'INVALID_FORMAT'; } return 'FORMAT_OK'; } const decisionCases = [ ['abc123', true, true, 'FORMAT_OK'], ['abc123', false, true, 'ACCOUNT_DISABLED'], ['abc123', true, false, 'SERVICE_UNAVAILABLE'], ['abc123', false, false, 'SERVICE_UNAVAILABLE'] ]; const lengthCases = [ [5, 'INVALID_FORMAT'], [6, 'FORMAT_OK'], [7, 'FORMAT_OK'], [11, 'FORMAT_OK'], [12, 'FORMAT_OK'], [13, 'INVALID_FORMAT'] ].map(([n, expected]) => ['a'.repeat(n), true, true, expected]); const formatCases = [ [null, true, true, 'INVALID_FORMAT'], ['abc 12', true, true, 'INVALID_FORMAT'], ['中文1234', true, true, 'INVALID_FORMAT'], ['abc12!', true, true, 'INVALID_FORMAT'], ['abc123\n', true, true, 'INVALID_FORMAT'], ['abc123', true, true, 'FORMAT_OK'] ]; const cases = [...decisionCases, ...lengthCases, ...formatCases]; for (const [password, enabled, online, expected] of cases) { assert.equal(assessInput(password, enabled, online), expected, JSON.stringify({password, enabled, online})); } console.log(`PASS: ${cases.length} decision/boundary/format cases`);node input-check.cjsNode.js 22.14 本次实际输出:
PASS: 16 decision/boundary/format cases字符检查使用 ASCII 范围,是为了与 R4 一致,不是因为 JavaScript 字符串长度普遍等于用户看到的字符数。这里检查“是否存在非法字符”,另行检查长度,尾部换行也不能混过去。若改写为带^、$的整串校验正则,不应随手加多行标志m,否则锚点也会匹配行边界,而不只整串边界,参见 MDN 输入边界断言。若真实需求接受 Unicode,要先决定按码元、码点还是用户感知字符计数,再设计用例。也不要在生产日志中输出密码;这里的断言信息只包含固定的合成样例。
为了检查验证器自身,本次另外把下界改成7、把两个状态检查交换、让标点绕过字符检查。三种错误版本都被这16条用例拒绝。变异被拒绝,只说明这些指定错误能被发现,不等于测试集已经完整。
5. 覆盖率要能追溯,也要敢写“还没测”
| 需求 | 本文证据 | 尚未证明的事 |
|---|---|---|
| R1/R2 | 4条判定表组合 | 真实服务故障时能否正确识别状态 |
| R3 | 6条长度边界、非字符串案例 | 输入框与接口是否使用同一计数规则 |
| R4 | 空格、标点、非ASCII案例 | 前端编码、传输、存储的一致性 |
这就是一个很小的需求跟踪表。需求有用例、用例有结果,是可追溯性的起点;“每条需求都有至少一个用例”不是“每条需求都充分验证”。不要把这两个比例都叫百分之百覆盖率。
真正的业务场景还需要请求、错误提示、后端认证、会话以及重试等层面。性能、弱网、兼容性可以用来补检查方向,但应说明目标环境与预期指标,不能用本地函数16条通过来代替这些验证。
错误猜测可以提出“实现是不是误用 trim”“前后端是否字符计数不同”等候选风险;最终仍要写出可执行前提和判定标准。没有需求依据的猜测,应先作为待确认问题,而非直接宣布产品有Bug。
6. 回到面试回答,也回到日常工作
我现在更愿意这样回答:先确认输入域、依赖状态和失败优先级;按等价类找代表,按边界找临界点,多条件用判定表;把预期写死成可执行断言,再用需求跟踪表注明已验证和仍缺的层面。
与测试分类与覆盖率反例一起看:分类帮助我找方向,用例帮助我检验具体行为。这里不追求“万能”,追求的是每条用例都有依据、失败能解释、结果能复现。