前言
做测试这些年,我见过太多同行写用例的方式:要么照着需求文档逐条翻译,要么凭感觉想到哪写到哪。结果呢?用例数量不少,但关键场景漏了、边界值没覆盖、重复用例一堆。
测试用例设计的核心问题不是"写多少",而是怎么用最少的用例覆盖最多的场景。等价类划分和边界值分析就是解决这个问题的两把利器。今天这篇文章,我会把这两项技术从原理到实战完整讲透,并补充我在项目中总结的实战技巧。
一、为什么需要等价类划分和边界值分析?
1.1 穷举测试是不现实的
假设一个输入框接受1-100的整数,如果穷举测试,需要测试100个有效值加上无数个无效值。这在时间和资源上根本不可行。
等价类划分和边界值分析的核心思想是:不需要测试每个值,只需要测试每个"等价类"的代表值和"边界"上的值。
1.2 缺陷聚集在边界
大量研究表明,软件缺陷往往聚集在输入范围的边界附近。比如:
- 一个接受1-100的输入框,bug最可能出现在输入0、1、100、101这些值时
- 一个接受6-10位密码的字段,bug最可能出现在5位、6位、10位、11位时
这就是边界值分析的理论基础:在边界处找bug,效率最高。
二、等价类划分:用分组代替穷举
2.1 什么是等价类划分?
等价类划分(Equivalence Partitioning,简称EP)是一种黑盒测试技术,它将输入数据划分为若干个"等价类"——同一等价类中的值,预期会被程序以相同方式处理。因此,每个等价类只需选取一个代表值进行测试。
2.2 等价类的分类
输入域
├── 有效等价类:符合需求的输入,程序应正常处理
└── 无效等价类:不符合需求的输入,程序应拒绝或提示错误
2.3 实战案例:披萨订购数量
原文中的经典案例:一个披萨订购系统,数量输入框接受1-10的整数。
不使用等价类: 需要测试1, 2, 3, ..., 10, 0, -1, 11, 12, ..., 99, 100, ... → 无穷无尽
使用等价类划分:
| 等价类 | 范围 | 代表值 | 预期结果 |
|---|---|---|---|
| 有效等价类 | 1-10 | 5 | 订购成功 |
| 无效等价类1 | 小于1 | 0 | 提示"数量不能小于1" |
| 无效等价类2 | 大于10 | 15 | 提示"最多订购10份" |
| 无效等价类3 | 非整数 | 3.5 | 提示"请输入整数" |
| 无效等价类4 | 非数字 | abc | 提示"请输入数字" |
从无穷多个测试值缩减到5个,覆盖率却不降反升——因为每个等价类中的值行为一致,测一个就等于测了全部。
2.4 等价类划分的适用范围
等价类划分不仅适用于数值输入,还适用于:
| 输入类型 | 划分方式 | 示例 |
|---|---|---|
| 数值范围 | 按范围划分有效/无效 | 年龄0-150:有效(0-150)、无效(负数、>150) |
| 字符串长度 | 按长度划分 | 密码6-20位:有效(6-20)、无效(0-5、21+) |
| 枚举值 | 每个值为一个等价类 | 性别:男、女、其他 |
| 集合 | 属于/不属于 | 会员等级:普通/银卡/金卡/钻石 |
| 布尔值 | true/false | 是否同意协议:是/否 |
三、边界值分析:在缺陷最密集的地方下功夫
3.1 什么是边界值分析?
边界值分析(Boundary Value Analysis,简称BVA)是对等价类划分的补充。它专门测试等价类边界上的值,因为边界是缺陷最容易出现的地方。
3.2 五点边界法
对于一个范围 [min, max],BVA测试以下5个关键点:
值: min-1 min min+1 ... max-1 max max+1
──────┼────┼──────────────┼────┼────┼──────
测试: ✗ ✓ ✓ ✓ ✓ ✗
无效 边界 边界内 边界内 边界 无效
| 测试点 | 取值 | 说明 |
|---|---|---|
| 略小于最小值 | min-1 | 无效边界,应被拒绝 |
| 最小值 | min | 有效边界,应被接受 |
| 略大于最小值 | min+1 | 有效值,验证边界内正常 |
| 略小于最大值 | max-1 | 有效值,验证边界内正常 |
| 最大值 | max | 有效边界,应被接受 |
| 略大于最大值 | max+1 | 无效边界,应被拒绝 |
3.3 实战案例:密码字段验证
密码字段要求6-10位字符,使用边界值分析:
| 测试场景 | 输入长度 | 预期结果 |
|---|---|---|
| 5位字符(边界外) | "abcde" | 拒绝,提示"密码至少6位" |
| 6位字符(下边界) | "abcdef" | 接受 |
| 7位字符(边界内) | "abcdefg" | 接受 |
| 9位字符(边界内) | "abcdefghi" | 接受 |
| 10位字符(上边界) | "abcdefghij" | 接受 |
| 11位字符(边界外) | "abcdefghijk" | 拒绝,提示"密码最多10位" |
我的实战补充: 很多测试人员只测min和max,忽略了min-1和max+1。但恰恰是这两个"刚好越界"的值,最容易暴露出off-by-one错误(差一错误)。比如开发写成了
if len(password) > 6而不是>= 6,6位密码就会被错误拒绝。
四、等价类划分 vs 边界值分析:如何配合使用?
两种技术不是替代关系,而是互补关系:
| 对比维度 | 等价类划分 | 边界值分析 |
|---|---|---|
| 关注点 | 输入的分组 | 分组的边界 |
| 数据选取 | 每个等价类取1个代表值 | 取min、min±1、max、max±1 |
| 擅长发现 | 功能逻辑错误 | off-by-one错误、边界处理缺陷 |
| 应用顺序 | 先划分等价类 | 再对每个等价类做边界分析 |
| 适用场景 | 减少冗余用例 | 捕获边界缺陷 |
最佳实践:先EP后BVA
Step 1: 用等价类划分确定有效/无效分组
Step 2: 对每个等价类的边界做边界值分析
Step 3: 合并去重,形成最终用例集
五、进阶实战:多字段组合的用例设计
单个字段的等价类和边界值相对简单,但实际项目中往往有多个输入字段需要组合测试。
5.1 案例:用户注册表单
注册表单包含以下字段:
| 字段 | 规则 |
|---|---|
| 用户名 | 4-20位字母数字 |
| 密码 | 8-20位,含大小写字母和数字 |
| 邮箱 | 合法邮箱格式 |
| 手机号 | 11位数字,1开头 |
Step 1:对每个字段做等价类划分
| 字段 | 有效等价类 | 无效等价类 |
|---|---|---|
| 用户名 | 4-20位字母数字 | <4位、>20位、含特殊字符、为空 |
| 密码 | 8-20位含大小写+数字 | <8位、>20位、纯小写、纯数字、为空 |
| 邮箱 | 合法格式 | 缺@、缺域名、格式错误、为空 |
| 手机号 | 11位1开头 | 非1开头、<11位、>11位、含字母、为空 |
Step 2:设计测试用例
不需要测试所有组合(4个字段各有5+个等价类,全组合=数千个),而是采用以下策略:
- 正常场景:所有字段取有效等价类代表值 → 1条用例
- 单字段异常:每次只有一个字段取无效等价类,其余取有效 → 每个无效等价类1条用例
- 关键组合:业务上常同时出错的组合 → 少量补充
# 用pytest参数化实现单字段异常测试
@pytest.mark.parametrize("field,value,expected_error", [
("username", "abc", "用户名至少4位"),
("username", "a" * 21, "用户名最多20位"),
("username", "test@user", "用户名只能包含字母和数字"),
("password", "Pass12", "密码至少8位"),
("password", "alllowercase1", "密码需包含大写字母"),
("email", "invalid-email", "邮箱格式不正确"),
("phone", "1234567890", "手机号必须11位"),
])
def test_register_invalid_field(field, value, expected_error):
valid_data = {
"username": "testuser01",
"password": "Pass1234",
"email": "test@example.com",
"phone": "13800000001"
}
valid_data[field] = value
resp = requests.post("/api/register", json=valid_data)
assert expected_error in resp.json()["message"]
5.2 鲁棒性边界值分析
标准BVA只测试有效范围边界,鲁棒性BVA(Robust BVA) 还会测试超出范围的值:
| 测试点 | 取值 | 说明 |
|---|---|---|
| min-1 | 刚好低于最小值 | 验证系统拒绝明显无效输入 |
| min | 最小值 | 有效边界 |
| nom | 中间值 | 正常值 |
| max | 最大值 | 有效边界 |
| max+1 | 刚好超过最大值 | 验证系统拒绝明显无效输入 |
我的经验: 鲁棒性BVA在金融、医疗等对数据准确性要求极高的系统中特别重要。比如金额输入框接受0.01-999999.99,不仅要测0.01和999999.99,还要测0.00和1000000.00,确保系统不会接受超出范围的值。
六、常见陷阱与避坑指南
陷阱1:等价类划分不完整
很多人只想到"太大"和"太小"两种无效等价类,忽略了其他类型:
输入:年龄(0-150的整数)
无效等价类:
✗ 负数(-1) ← 大多数人会想到
✗ 大于150(200) ← 大多数人会想到
✗ 小数(3.5) ← 容易遗漏!
✗ 非数字(abc) ← 容易遗漏!
✗ 空值("") ← 容易遗漏!
✗ 特殊字符(<script>) ← 容易遗漏!
陷阱2:边界值只取整数边界
如果输入范围是小数呢?比如折扣率0.01-0.99:
边界值:0.00, 0.01, 0.02, 0.98, 0.99, 1.00
不要只测:0, 1, 99, 100(这是整数思维)
陷阱3:忽视输出等价类
等价类划分不仅适用于输入,也适用于输出。比如一个计算折扣的函数:
输入:消费金额
输出:折扣率
消费 < 100 → 无折扣
100 ≤ 消费 < 500 → 9折
500 ≤ 消费 < 1000 → 8折
消费 ≥ 1000 → 7折
输出等价类边界:99, 100, 499, 500, 999, 1000
陷阱4:过度依赖单一技术
等价类和边界值主要针对单个输入字段。对于多字段组合逻辑、业务流程等场景,还需要配合:
- 判定表:处理多条件组合
- 场景法:处理业务流程
- 状态迁移测试:处理状态变化
七、自动化实践:用pytest实现参数化边界测试
import pytest
class TestDiscountCalculation:
"""折扣计算 - 等价类+边界值自动化测试"""
def calculate_discount(self, amount):
"""被测函数:根据消费金额计算折扣"""
if amount < 0:
raise ValueError("金额不能为负数")
if amount < 100:
return 1.0 # 无折扣
elif amount < 500:
return 0.9 # 9折
elif amount < 1000:
return 0.8 # 8折
else:
return 0.7 # 7折
# 等价类划分测试
@pytest.mark.parametrize("amount,expected_discount", [
(50, 1.0), # 有效等价类:< 100
(200, 0.9), # 有效等价类:100-499
(700, 0.8), # 有效等价类:500-999
(1500, 0.7), # 有效等价类:≥ 1000
])
def test_equivalence_classes(self, amount, expected_discount):
result = self.calculate_discount(amount)
assert result == expected_discount
# 边界值分析测试
@pytest.mark.parametrize("amount,expected_discount", [
(0, 1.0), # 边界:最小值
(99, 1.0), # 边界:无折扣上限
(100, 0.9), # 边界:9折下限
(499, 0.9), # 边界:9折上限
(500, 0.8), # 边界:8折下限
(999, 0.8), # 边界:8折上限
(1000, 0.7), # 边界:7折下限
])
def test_boundary_values(self, amount, expected_discount):
result = self.calculate_discount(amount)
assert result == expected_discount
# 无效等价类测试
def test_negative_amount(self):
with pytest.raises(ValueError, match="金额不能为负数"):
self.calculate_discount(-1)
总结
| 技术 | 核心思想 | 适用场景 | 关键动作 |
|---|---|---|---|
| 等价类划分 | 分组取代表 | 减少冗余用例 | 划分有效/无效等价类,每类取1个代表值 |
| 边界值分析 | 边界找缺陷 | 捕获off-by-one错误 | 测试min、min±1、max、max±1 |
| 鲁棒性BVA | 越界验证 | 高可靠性系统 | 在BVA基础上增加min-1和max+1 |
记住这个口诀:先划分等价类,再分析边界值,最后用自动化参数化落地。