第3章 等价类划分:让测试用例告别无效劳动

你有没有遇到过这种情况?一个功能测了几十个用例,线上还是出了Bug。更郁闷的是——出问题的那个输入,你压根儿就没测到。

说白了,这就是测试的「穷举困境」。一个简单的输入框,组合起来可能成千上万种情况。真要把所有值都测一遍,项目周期根本不允许。那怎么办?

我在早期做模块验证时,也被这个问题困扰过。后来搞懂了等价类划分,才算是真正找到了突破口。这玩意儿不花哨,但极其实用。

3.1 为什么需要等价类划分

核心思想其实一句话:把无限输入,压缩成有限代表。

你想想看,对于同一个功能来说,很多输入其实是「等效」的。它们走的是同一段代码逻辑,触发的也是同样的处理路径。既然效果一样,那每类只测一个值就行了。

举个例子:一个年龄输入框,要求 1~120 岁。你测 25 和测 60,本质上有区别吗?没有。它们都属于「有效范围」这一类,代码处理逻辑完全一致。

等价类划分就是帮你做这件事——识别出哪些输入是一家人,然后每个家庭只派一个代表去测试。

核心价值:用最少的用例,覆盖最全的场景。效率翻倍,漏测减半。

3.2 等价类的两大类型

等价类分两种,缺一不可。我在项目中吃过亏,只关注有效类,忽略了无效类,结果上线后被各种异常输入搞崩溃了。

  • 有效等价类:符合需求文档的合法输入。比如年龄 1~120 岁之间的任意整数。
  • 无效等价类:不符合需求的非法输入。比如年龄 0、-5、200、"abc"、空值等等。

为什么无效类这么重要?因为用户不会按你的规则来。他们可能输入任何东西。你的代码扛得住吗?

注意:无效等价类往往不止一个。比如年龄输入,小于1是一类,大于120是另一类,非数字又是另一类。每类都要覆盖到。

3.3 划分原则与实战步骤

怎么划分?我总结了一套实操方法,按这5条原则来,基本不会漏。

划分原则

  1. 按范围划分:输入是一个区间,就拆成「低于区间」「在区间内」「高于区间」三类。
  2. 按集合划分:输入来自一个枚举集合,集合内每个元素算一类,集合外算一类。
  3. 按布尔条件划分:条件成立一类,不成立一类。
  4. 按输入格式划分:合法格式一类,非法格式一类(如邮箱、手机号)。
  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}")
我的习惯:写测试用例时,我会把等价类编号写在注释里,比如 "EC01-01: 有效等价类-代表值"。这样出了问题能快速追溯到是哪个等价类漏了。

3.5 避坑指南

这些年用等价类划分,我踩过不少坑。挑几个典型的说说:

  • 别把无效等价类合并成一个。我曾经觉得 age<1 和 age>120 都是「范围外」,合并成一个用例来测。结果代码对小于1的处理有Bug,但对大于120的处理没问题。合并测根本发现不了。——每个无效等价类必须单独测。
  • 注意组合等价类。单独一个输入字段没问题,但几个字段组合起来可能产生新的等价场景。比如年龄和身份的组合,未成年+某些身份可能触发特殊逻辑。
  • 别忽略「空值」和「默认值」。这俩是最容易被遗忘的无效等价类。我见过太多因为没传参数导致系统崩溃的线上事故了。
  • 边界值要「里外都测」。只测边界本身不够,边界的「邻居」也要测。比如边界是1,那0和2都要测。
提醒:等价类划分不是万能的。它主要针对「输入空间」的覆盖。对于流程、状态、时序等问题,还需要结合其他测试方法。
等价类划分实战流程 分析输入条件 识别所有输入维度 划分等价类 按规则分组 区分有效/无效 标注类型 选取代表值 每类取一个 设计测试用例 覆盖所有类 补充边界值 关注边界 💡 等价类划分核心要点 • 有效等价类 + 无效等价类,两者都要覆盖,缺一不可 • 每个无效等价类必须单独设计用例,不能合并 • 边界值分析是等价类划分的最佳补充,重点关注边界两侧

等价类划分不是什么高深理论。它就是一个实用工具,帮你在有限的时间和资源下,把测试覆盖做到最大。我到现在做模块验证,第一件事仍然是拉个表格,把输入条件的等价类一个一个列出来。这个习惯,帮我省了太多事了。

记住一句话:测得多不如测得巧。等价类划分,就是让你「测得巧」的那把钥匙。