第3章 等价类划分:让测试用例告别无效劳动
你有没有遇到过这种情况?一个功能测了几十个用例,线上还是出了Bug。更郁闷的是——出问题的那个输入,你压根儿就没测到。
说白了,这就是测试的「穷举困境」。一个简单的输入框,组合起来可能成千上万种情况。真要把所有值都测一遍,项目周期根本不允许。那怎么办?
我在早期做模块验证时,也被这个问题困扰过。后来搞懂了等价类划分,才算是真正找到了突破口。这玩意儿不花哨,但极其实用。
3.1 为什么需要等价类划分
核心思想其实一句话:把无限输入,压缩成有限代表。
你想想看,对于同一个功能来说,很多输入其实是「等效」的。它们走的是同一段代码逻辑,触发的也是同样的处理路径。既然效果一样,那每类只测一个值就行了。
举个例子:一个年龄输入框,要求 1~120 岁。你测 25 和测 60,本质上有区别吗?没有。它们都属于「有效范围」这一类,代码处理逻辑完全一致。
等价类划分就是帮你做这件事——识别出哪些输入是一家人,然后每个家庭只派一个代表去测试。
3.2 等价类的两大类型
等价类分两种,缺一不可。我在项目中吃过亏,只关注有效类,忽略了无效类,结果上线后被各种异常输入搞崩溃了。
- 有效等价类:符合需求文档的合法输入。比如年龄 1~120 岁之间的任意整数。
- 无效等价类:不符合需求的非法输入。比如年龄 0、-5、200、"abc"、空值等等。
为什么无效类这么重要?因为用户不会按你的规则来。他们可能输入任何东西。你的代码扛得住吗?
3.3 划分原则与实战步骤
怎么划分?我总结了一套实操方法,按这5条原则来,基本不会漏。
划分原则
- 按范围划分:输入是一个区间,就拆成「低于区间」「在区间内」「高于区间」三类。
- 按集合划分:输入来自一个枚举集合,集合内每个元素算一类,集合外算一类。
- 按布尔条件划分:条件成立一类,不成立一类。
- 按输入格式划分:合法格式一类,非法格式一类(如邮箱、手机号)。
- 按边界值补充:每个等价类的边界点单独拎出来测(下一节细讲)。
实战四步法
我个人习惯按这四步走,清晰又高效:
| 步骤 | 做什么 | 输出 |
|---|---|---|
| 第一步 | 列出所有输入条件 | 输入清单 |
| 第二步 | 每个条件划分有效/无效等价类 | 等价类列表 |
| 第三步 | 为每类选取一个代表值 | 代表值清单 |
| 第四步 | 补充边界值作为额外用例 | 最终测试用例 |
举个例子,还是年龄输入框:
| 输入条件 | 等价类 | 类型 | 代表值 |
|---|---|---|---|
| 年龄范围 1~120 | 1 ≤ age ≤ 120 | 有效 | 25 |
| 年龄范围 1~120 | age < 1 | 无效 | 0 |
| 年龄范围 1~120 | age > 120 | 无效 | 200 |
| 年龄范围 1~120 | 非数字输入 | 无效 | "abc" |
| 年龄范围 1~120 | 空值 | 无效 | null |
3.4 边界值分析:等价类的最佳拍档
等价类划分有一个弱点——它不特别关注边界。而经验告诉我,Bug最密集的地方,恰恰就在边界上。
为什么会这样?因为开发写条件判断时,很容易把 <= 写成 <,或者把边界值漏掉。这种错误靠等价类的代表值是测不出来的。
所以我的做法是:等价类划分 + 边界值分析,两者结合用。
边界值选取策略
- 每个等价类取 最小值、最大值、略小于最小值、略大于最大值 四个点
- 对于有效等价类 1~120,边界值就是:0、1、120、121
- 对于无效等价类,边界值就是它的端点值
来看一段Python示例,演示如何基于等价类划分为年龄验证函数设计测试用例:
# 被测试函数
def validate_age(age):
if age is None:
return "年龄不能为空"
if not isinstance(age, int):
return "年龄必须为整数"
if age < 1 or age > 120:
return "年龄必须在1-120之间"
return "验证通过"
# 基于等价类划分的测试用例
test_cases = [
# 有效等价类
(25, "验证通过"), # 代表值
(1, "验证通过"), # 边界:最小值
(120, "验证通过"), # 边界:最大值
# 无效等价类:小于1
(0, "年龄必须在1-120之间"), # 边界
(-1, "年龄必须在1-120之间"),
# 无效等价类:大于120
(121, "年龄必须在1-120之间"), # 边界
(200, "年龄必须在1-120之间"),
# 无效等价类:非数字
("abc", "年龄必须为整数"),
(3.14, "年龄必须为整数"),
# 无效等价类:空值
(None, "年龄不能为空"),
]
for age, expected in test_cases:
result = validate_age(age)
status = "✅" if result == expected else "❌"
print(f"{status} age={age} → {result}")
3.5 避坑指南
这些年用等价类划分,我踩过不少坑。挑几个典型的说说:
- 别把无效等价类合并成一个。我曾经觉得 age<1 和 age>120 都是「范围外」,合并成一个用例来测。结果代码对小于1的处理有Bug,但对大于120的处理没问题。合并测根本发现不了。——每个无效等价类必须单独测。
- 注意组合等价类。单独一个输入字段没问题,但几个字段组合起来可能产生新的等价场景。比如年龄和身份的组合,未成年+某些身份可能触发特殊逻辑。
- 别忽略「空值」和「默认值」。这俩是最容易被遗忘的无效等价类。我见过太多因为没传参数导致系统崩溃的线上事故了。
- 边界值要「里外都测」。只测边界本身不够,边界的「邻居」也要测。比如边界是1,那0和2都要测。
等价类划分不是什么高深理论。它就是一个实用工具,帮你在有限的时间和资源下,把测试覆盖做到最大。我到现在做模块验证,第一件事仍然是拉个表格,把输入条件的等价类一个一个列出来。这个习惯,帮我省了太多事了。
记住一句话:测得多不如测得巧。等价类划分,就是让你「测得巧」的那把钥匙。