- 用最少的测试用例,覆盖最多的输入风险。
- 思路:把输入分成等价类(Equivalence Class),在每个关键范围处做边界值(Boundary Value)验证,并补齐格式/编码/跨字段的关键约束。
- Treeify 专注把测试设计变成可建模、可评审、可持续迭代的过程——用结构化方法把问题空间拆开,再生成更少但更有覆盖的用例。
想一起把测试设计做得更工程化,欢迎来共创/内测。- 添加 V:【TreeifyAI】进内测共创群,获得 Treeify 内测资格 / 免费 credits / MCP Server 试用
1. 文档的目标与适用对象
这份文档面向希望系统性掌握输入类测试设计的读者,包括:
- 新入行的测试工程师
- 负责需求评审、测试策略与测试用例设计的 QA
- 需要更高可追溯(Traceable)与覆盖(Coverage)的技术负责人
很多团队已经在“靠经验补用例”,但:
- 不清楚当前字段到底有哪些约束。
- 不确定“是不是所有关键边界都测到了”。
- 用例写完很难和需求、接口契约一一对应。
阅读本篇后,你应该能够实际做到:
- 系统识别输入字段的约束(范围、格式、长度、模式、跨字段关系)。
- 正确拆分等价类,并按“最少代表输入”设计用例。
- 使用边界值捕获 off-by-one、溢出、格式误差等高风险缺陷。
- 设计跨字段(Cross-field)约束的边界测试,而不是只盯单字段。
- 产出结构化、可追溯、有明确期望与证据(Oracle)的高质量用例集。
2. 核心概念
2.1 等价类(Equivalence Class)
等价类指:一组输入在系统中应表现为相同结果。
在同一个等价类中,系统行为(通过 / 拒绝 / 报错类型)应完全一致,因此测试只需选 1–2 个代表值即可,而不需要穷举所有可能值。
示例:金额字段0..10000
有效类(举例):
- 合法范围内的整数:0、1、9999、10000
- 它们都应该“通过校验 + 正确入库”
无效类(举例):
- 范围外:-1、10001
- 类型错误:
12.34、"abc" - 缺失:
null、空字符串
每一个“有效类/无效类”对应一个期望行为,而不是一个具体值。
用例重点在“覆盖所有类”,不是“把所有数字测一遍”。
实用判断标准:
如果你把某个输入换成同一类中的另一个值,系统行为不应发生变化,否则说明等价类划分还不够细。
2.2 边界值(Boundary Value)
边界值是指输入在范围边缘附近的具体值,通常包括:
数值:
- 最小值、最小值 ±1
- 最大值、最大值 ±1
- 必要时加入一个中间值作为“正常样本”
字符串:
- 最短长度、最短长度 ±1
- 最长长度、最长长度 ±1
与业务边界相关的值:
- 刚好过期时间、刚好截止时间
- 容量上限、额度上限等
多数输入相关错误(off-by-one、溢出、截断、边界条件判断错误)都发生在边界,
因此边界值测试是低成本高收益的策略。
2.3 为什么要结合两者?
- 等价类负责控制数量:避免大量重复、低价值的中间值。
- 边界值负责提高敏感度:重点验证每个类的边缘行为。
合在一起,一般可以在8–12 个测试用例内覆盖掉输入相关的大部分缺陷。
重点不在“多”,而在于:
- 类是否划分得合理。
- 每个类的边界是否被覆盖。
- 每个用例是否有清晰的预期与可验证的证据。
3. 完整设计流程(7 步)
下面的 7 步流程可以直接落地到日常需求上,不依赖特定工具。
第 1 步:提取字段约束
从 PRD / 设计文档 / 接口契约 / 现有代码中抽取所有约束,包括:
范围(如
0..10000),是否包含边界(>=或>)。格式:
- 字符集(只允许数字?字母?
[A-Z0-9-]?) - 正则表达式(如邮箱、手机号)
- 字符集(只允许数字?字母?
长度:最小、最大、是否有“推荐长度”。
是否可空:允许
null/ 空字符串吗?空白(仅空格)算不算空?预处理规则:是否会 trim?会不会统一大小写?
编码与规范化:如 Unicode NFC/NFD 的处理策略。
单位与精度:金额单位是“元”还是“分”?小数保留几位?
舍入规则:四舍五入、向上取整、向下取整。
如果约束没有被确认清楚,后续的等价类划分一定不完整。
所以这一步和需求澄清、API 契约评审是强相关的。
第 2 步:划分等价类(需 MECE)
MECE(互斥且完全)要求:
- 每个输入只能落入一个类(互斥,无重叠)
- 所有可能输入都能被某个类覆盖(完全,无遗漏)
典型拆分方式:
- 有效 vs 无效
- 范围内 vs 范围外
- 符合格式 vs 不符合格式
- 空值 / 空白 / 缺失
- 特殊字符 / 特殊编码(emoji、非拉丁字母等)
实际操作时,可以先只画两大块:有效和无效,
再分别细分:
- 有效类里细分不同子区间(如 0、1…9999、10000)。
- 无效类里细分“低于最小、高于最大、类型错误、空值”等。
第 3 步:生成边界值集合
在完成等价类之后,为“带范围的类”生成边界值集合。
建议使用标准模板:
min − 1(如果业务允许产生)minmin + 1- 中间值(mid,可选,但对验证“正常情况”有帮助)
max − 1maxmax + 1(如果业务允许产生)
示例:0..10000→
[-1, 0, 1, 5000, 9999, 10000, 10001]
之后再根据业务含义对这些值进行筛选与精简,保留能触发不同行为的部分即可。
第 4 步:补齐格式 / 编码边缘
范围边界只是输入的一部分,实际系统中经常有大量“格式类”问题:
前后空格:
" SAVE10 "是否允许?是否 trim?
内部空格:
"SAVE 10"是否视为非法?
大小写:
"save10"与"SAVE10"是否等价?是否统一存大写?
Unicode:
- emoji、非拉丁字符(中文、阿拉伯文等)是否允许?
正规化(NFC/NFD):
- 例如
é(字母+组合音标) vsé(单一字符)。
- 例如
特殊不可见字符:零宽空格等。
这些“边缘格式”在数据库、搜索、排序、日志中常产生隐蔽问题,
因此建议在每个关键字段上至少选 2–3 个格式边缘用例。
第 5 步:处理跨字段(Cross-field)约束
跨字段的约束不是“可选加分项”,而是业务错误的常见来源。
典型场景:
金额之间:
- 优惠金额 ≤ 订单小计
- 退款金额 ≤ 已支付金额
时间之间:
- 开始时间 ≤ 结束时间
- 创建时间 ≤ 完成时间
级别与权限:
- 用户级别 ≥ 功能所需级别
数量:
- 购买数量 ≤ 库存
- 单次下单数量上限
在设计这些场景时,可以固定一个字段,把另一个字段沿边界移动(−1、0、+1),比如:
- 小计固定为 100:折扣取 0、100、101。
- 结束时间固定:开始时间取 =结束时间、略大于、略小于。
这样可以保证:
至少验证一次“刚好相等”的情况 + 一次“略超出”的情况。
第 6 步:明确期望结果与验证依据(Oracle)
每个用例必须写清:
- 结果是:通过/拒绝/部分接受(例如:自动 trim 后再验证)。
- 返回什么:HTTP 状态码、业务错误码、错误分类(Validation/Conflict/Auth 等)。
- 数据层行为:是否写库?是否幂等地更新?是否生成事件。
- 前端变化:文本提示(最好用文案 ID 或错误码映射)、按钮状态、字段高亮等。
- 证据来源:日志字段(
error_code等)、metrics、trace、截图。
否则你会很难在回归时确认:
“这个用例是产品改了预期,还是代码出 bug?”
第 7 步:去重、压缩
最后,对所有候选用例做一次“行为去重”:
- 如果两个用例落在同一等价类,触发的行为完全一致,就只保留一个即可。
- 对仅用于“演示”的中间值(如多个普通合法值),通常只保留 1 个。
- 保留所有边界值,尤其是 ±1 点。
- 对格式边缘(空格、大写、Unicode),保留最能代表差异的 2–4 条。
目标是在 8–12 条中保留“行为差异明显”的用例,而不是机械地保留所有边界生成结果。
4. 示例 A:金额字段(0…10,000,整数)
4.1 约束整理
假设某金额字段(如“单笔支付金额”)约束如下:
- 范围:整数
0..10000,含 0 和 10000。 - 类型:必须为整数(不允许小数)。
- 单位:入库以“分”为单位(×100)。
- 空值/空串:视为校验失败,不允许默认为 0。
- 负数:一律非法。
- 上层业务限制:可能还存在“每日限额”等约束(可在跨字段中处理)。
4.2 等价类与边界值
| 类别 | 代表值 | 设计原因 |
|---|---|---|
| 小于最小值 | -1 | 典型下溢、off-by-one |
| 最小值 | 0 | 验证下边界是否包括 |
| 最小值+1 | 1 | 区分>0与>=0的实现差异 |
| 中间值 | 1234 | 正常样本,验证主流程 |
| 最大值-1 | 9999 | 验证上边界附近 |
| 最大值 | 10000 | 验证上边界是否包括 |
| 大于最大值 | 10001 | 溢出 / 越界 |
| 非整数 | 12.34、"100.0" | 类型转化 / 解析错误 |
| 空值/空白 | ""、" "、null | 必填校验 |
| 非数字字符 | "ABC"、"10元" | 格式与字符集校验 |
可以看到,上面并没有引入大量中间值,只是围绕边界与典型值做覆盖。
4.3 用例片段(带 Oracle)
接受
0期望:通过校验,写入
0分;显示金额为0.00。Oracle:
- 响应码 200
- 数据库字段
amount_in_cents = 0 - 日志中记录
amount=0,无错误码
拒绝
-1期望:校验失败,返回
VALIDATION.amount.min,不给写库。Oracle:
- 响应码 400 或 422
error_code = VALIDATION.amount.min- 数据库存量保持不变
接受
10000期望:通过校验,写入
1000000分。Oracle:
- 响应 200
- 入库值正确(已换算)
- 日志中记录
amount_raw=10000, amount_in_cents=1000000
拒绝
10001(上溢)期望:校验失败,返回
VALIDATION.amount.max。Oracle:
- 响应 400/422
- 无写库
- 日志中有错误记录,含
error_code
拒绝
12.34(类型错误)期望:类型错误,要求整数。
Oracle:
- 错误码
VALIDATION.amount.type.integer - 无写库
- 错误码
拒绝空值(必填)
期望:错误码
VALIDATION.amount.required。Oracle:
- 响应 400/422
- 日志中记录字段缺失或为空
5. 示例 B:优惠码(长度 1…16、字符集[A-Z0-9-]、大小写不敏感、trim)
5.1 约束整理
假设优惠码字段约束如下:
- 长度:1–16 字符。
- 字符集:大写字母
A-Z、数字0-9、连字符-。 - 大小写不敏感:内部存储统一转为大写。
- 前后空格:自动 trim。
- 内部空格:不允许。
- 编码:使用 Unicode NFC 存储,避免同一字符多种编码导致比较问题。
5.2 类别与边界
| 维度 | 类别/边缘 | 示例 | 说明 |
|---|---|---|---|
| 长度 | 空值 | "" | 不能为空 |
| 最小值 | "A" | 下边界 | |
| 常规 | "SAVE10" | 正常合法码 | |
| 最大值 | "A"*16(16 个字符) | 上边界 | |
| 超长 | "A"*17(17 个字符) | 越界 | |
| 字符集 | 合法 | "A1-B2" | 允许的字符组合 |
| 非法字符 | "SAVE!0" | 感叹号不在允许范围 | |
| emoji/非拉丁 | "SAVE"、"减10" | 非预期字符集 | |
| 空格 | 前后空格(可 trim) | " SAVE10 " | 预期被 trim |
| 内部空格(非法) | "SAVE 10" | 不允许内部空格 | |
| 大小写 | 正常化 | "save10" | 转为"SAVE10"保存与比较 |
| Unicode | NFC/NFD | évsé | 应有一致处理策略 |
5.3 用例片段
"A"(长度=1)应接受期望:合法优惠码,存储为
"A"。Oracle:
- 响应 200
- 存储字段为大写
"A",长度 1
" SAVE10 "→ trim 后接受"SAVE10"期望:前后空格被去除,内部值合法。
Oracle:
- 响应 200
- 存储值
"SAVE10",长度为 6,无前后空格
"SAVE 10"→ 因内部空格拒绝期望:返回
VALIDATION.code.charset(或等效错误码),不给写库。Oracle:
- 响应 400/422
- 错误码标识为“非法字符”
- 不将该值写入数据库
"A"*17(17 字符)→ 超长拒绝期望:错误码
VALIDATION.code.length.exceeds。Oracle:
- 响应 400/422
- 无写库
"SAVE!0"→ 非法字符拒绝- 期望:错误码
VALIDATION.code.charset。
- 期望:错误码
"save10"→ 小写应接受,并存大写期望:成功,通过时内部转换为
"SAVE10"。Oracle:
- 存储为大写
- 后续比较一律采用规范化后的值
NFC/NFD 两形式(如
é)期望:根据系统策略,要么都拒绝,要么都按统一规范处理。
Oracle:
- 如果允许 Unicode,则入库前统一正规化(如转 NFC),两者行为一致。
- 如果不允许 Unicode,则统一拒绝。
6. 示例 C:跨字段约束
跨字段约束可以视作“额外的维度的边界测试”。
高风险场景往往集中在“刚好等于”“刚好超过”的位置。
6.1 折扣 ≤ 小计
假设:
- 订单小计(subtotal)= 100
- 规则:折扣金额
0 ≤ discount ≤ subtotal; - 业务约定:折扣金额不可超过小计,是否允许“等于”由产品定义。
可以设计以下测试点:
- 折扣 = -1(非法:小于 0)
- 折扣 = 0(合法)
- 折扣 = 1(合法)
- 折扣 = 99(合法)
- 折扣 = 100(等于小计,根据业务定义:允许或拒绝,要明确)
- 折扣 = 101(非法:超过小计)
关键在于说明“等于”的业务策略,否则实现和测试容易出现理解偏差。
6.2 日期:开始时间 ≤ 结束时间
假设约束:
start_time ≤ end_time- 时间带有时区信息。
可设计的边界点:
start = end:合法,用于“瞬时事件”或“单日活动”。start = end + 1s:应失败,返回“时间区间错误”类错误。start < end:常规合法区间。跨时区场景:
- 不同时区的 start / end 映射回同一 UTC 时,保证比较逻辑正确。
- 涉及夏令时(DST)的地区,需检查“跳时”当天的行为。
7. 如何快速生成边界集合
在日常工作中,可以借助简单脚本生成候选边界值,然后再经过人工筛选。
def boundaries(min_v, max_v, step=1): """ 生成基础数值边界集合。 注意:生成的是“候选集合”,需要人工根据业务语义做筛选。 """ mid = (min_v + max_v) // 2 return [min_v - step, min_v, min_v + step, mid, max_v - step, max_v, max_v + step] # 示例:0..10000 → [-1, 0, 1, 5000, 9999, 10000, 10001]重要提醒:
生成工具只是帮助你“想全”,并不能自动给出“最终用例列表”。
需要再结合:
- 等价类划分
- 业务规则
- 跨字段约束
手动删除行为重复的点,保留最有价值的代表值。
8. 期望结果与证据(Oracle)说明
一个高质量的输入类测试用例,至少要包含下面几类信息中的一部分:
功能结果
- HTTP 状态码
- 业务错误码
- 返回体结构(字段存在/缺失、类型)
- 数据库变更(写入成功/拒绝写入/更新策略)
UX 行为
- 指定 ID 的提示语
- 字段是否高亮
- 按钮是否禁用
- 表单是否保持原输入(例如校验失败时不清空)
API/契约
- 错误码来自统一错误分类(如
VALIDATION.*、CONFLICT.*) - 对幂等请求的处理(同 key 是否返回同结果)
- 错误码来自统一错误分类(如
非功能
- p95 延迟是否在预算内
- 重试次数是否在预期范围
- 是否触发熔断 / 限流等机制
证据
- 结构化日志:包含
correlation_id、error_code等字段 - 指标:请求计数、错误计数
- Trace:调用链路中关键 span
- 截图 / 导出文件(用于人工复核)
- 结构化日志:包含
有了明确的 Oracle 和证据点,用例才能在自动化、复盘、事故分析中持续发挥价值,而不只是一份“描述性的文档”。
9. 常见误区
写大量中间正常值
- 问题:行为完全一样,只是浪费执行时间。
- 建议:主流程保留 1–2 个典型值即可。
忽略 ±1 边界
- off-by-one 错误往往发生在这些位置,忽略它们会明显降低测试效果。
把“包含”写成“不包含”或相反
- 表现在:需求说
0..10000,实现写成0 ≤ x < 10000或0 < x ≤ 10000。 - 建议:在用例中显式包含两个端点值(0 和 10000),并与产品/开发确认期望。
- 表现在:需求说
忽略单位换算
- 例如前端显示“元”,后端存“分”,中间换算容易出错。
- 建议:为换算逻辑写独立用例。
漏掉跨字段约束
- 常见于金额、时间、权限、数量相关逻辑。
- 建议:对关键约束至少写一条“刚好等于”和一条“略超出”的用例。
无 Oracle
- 只有“操作步骤”和“模糊期待”,导致后期难以确认是否通过。
- 建议:每条用例必须写明“如何判断结果”,至少包括错误码/状态/日志中的某个字段。
忽略 trim、大小写、Unicode 标准化
- 在多终端、多语言环境中,这类问题发生概率很高,但常被忽视。
- 建议:对于用户可输入的关键字段,至少覆盖 2–3 个格式/编码边界用例。
10. 用例审查检查清单(闸口)
- 所有约束已识别(范围/长度/字符集/trim/大小写/编码/单位/舍入)
- 等价类划分满足 MECE(互斥且完全)
- 数值类字段包含
min−1, min, min+1, max−1, max, max+1的代表性用例 - 覆盖格式/编码边缘(空格、大小写、Unicode、NFC/NFD 等)
- 跨字段约束(金额、时间、权限等)已测试
- 每条用例有明确期望与 Oracle
- 日志/指标/trace 中有可用的证据点
- 多余的中间值已删除(用例数量可控)
这份清单可以直接作为评审时的“快速闸口”,有助于把讨论从“感觉够不够”转成“约束和边界是否覆盖完整”。
11. CSV 示例(可用于种子数据)
金额(0…10000)
id,input,klass,expected,oracle A-001,-1,below_min,400 VALIDATION.amount.min,resp400+no_db_write A-002,0,min,200 OK,resp200+db=0 A-003,1,just_above_min,200 OK,resp200+db=1 A-004,9999,just_below_max,200 OK,resp200+db=9999 A-005,10000,max,200 OK,resp200+db=10000 A-006,10001,above_max,400 VALIDATION.amount.max,resp400+no_db_write A-007,12.34,non_integer,400 VALIDATION.amount.type.integer,resp400 A-008,"",empty,400 VALIDATION.amount.required,resp400优惠码(1…16[A-Z0-9-])
id,input,klass,expected,oracle C-001,"A",min,accept,resp200+store=uppercase C-002," SAVE10 ",trim,accept,resp200+store=SAVE10 C-003,"SAVE 10",inner_space,reject,400 VALIDATION.code.charset C-004,"AAAAAAAAAAAAAAAA",above_max,reject,400 VALIDATION.code.length.exceeds C-005,"SAVE!0",forbidden,reject,400 VALIDATION.code.charset C-006,"save10",lowercase_norm,accept,resp200+store=SAVE10这些 CSV 可以直接作为:
- 种子测试数据
- 自动化测试的输入驱动
- 团队内部教学示例