边界情况 · 软件测试
应该测试哪些输入?
课程目录为什么需要它
你不可能测试所有可能的输入,而且大多数 bug 不在范围的中间。它们藏在边界上:空列表、正好等于上限、超出一点点、零。用心挑几个输入,比随便测一百个抓到的 bug 更多。
直觉理解
- 把输入分组,同一组的输入行为应该相同(合法 / 太短 / 太长;正数 / 零 / 负数)。每组通常一个测试就够了。
- 测试组和组之间的边界:如果规则是「8 到 64 个字符」,就测 7、8、64 和 65。差一错误(把
<=写成<)只会在这里暴露。 - 测试特殊值:空字符串或空列表、一个元素、零、负数、非常大的数、
None。 - 也要测试非法输入。如果函数应该报错,就检查它确实报错了:
def test_rejects_negative():
try:
sqrt(-1)
except ValueError:
return # 正是我们想要的错误
assert False, "expected ValueError"(pytest 里写成 with pytest.raises(ValueError): sqrt(-1)。)
想检验你的测试够不够强,就设想代码里出了一些小错误(<= 写成 <、少了一条规则)。每个错误都应该至少让一个测试失败。自动做这件事的工具叫变异测试。
定义
边界值
An input exactly at, just below or just above a limit, where off-by-one bugs appear.正好在限制上、刚好低于或刚好高于限制的输入,差一错误就出现在这里。
等价类
A group of inputs the code should treat the same way; testing one of them stands for the group.代码应该同样对待的一组输入;测试其中一个就能代表整组。
变异测试
Checking tests by making small deliberate bugs (mutants) and seeing whether some test fails.故意制造小 bug(变异体),看有没有测试失败,以此检验测试本身。
示例
找边界上的 bug
动手试试每个区间的中间都没问题;正好 90 就不对了。
def grade(score):
if score > 90:
return "A"
if score >= 75:
return "B"
return "C"
for score in [95, 80, 50, 90, 75, 74]:
print(score, grade(score))95 A 80 B 50 C 90 B 75 B 74 C
- L2规则说 90 分及以上是 A,但这里写的是高于 90。
- L895、80、50 都在各组中间,看起来没问题。90 是边界:给了 B,错了。
测试会不会报错
动手试试出现正确的错误时测试通过,没有出现时测试失败。
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("amount must be positive")
return balance - amount
def raises(fn, error):
try:
fn()
except error:
return True
return False
print(raises(lambda: withdraw(100, -5), ValueError))
print(raises(lambda: withdraw(100, 0), ValueError))
print(raises(lambda: withdraw(100, 5), ValueError))True True False
- L6一个小工具:运行 fn,告诉你它有没有抛出那种错误。
- L13lambda 把调用推迟,好让 raises() 在 try 里面运行它。
- L140 是边界:它也应该被拒绝。
- L15合法金额:没有错误。
False
常见错误
只测典型值
即使 90 那里有问题,50 和 95 也能通过。每个界限处和刚越过界限的地方都要放一个测试。
忘了空的情况
空字符串和空列表会导致崩溃(取不存在的第 0 个)和除以零。要专门测试它们。
测试报错却什么也没检查
try: f() except ValueError: pass 即使 f() 什么都没抛也能通过。最后要加 assert False(或者用 pytest.raises)。
小测验
1. 表单接受 13 到 120 岁。哪组测试值最好?
查看答案
答案:A
每个界限,以及越过每个界限一步的值。
2. 这段代码会输出什么?
def is_teen(age):
return 13 < age < 20
print([age for age in [12, 13, 19, 20] if is_teen(age)])查看答案
答案:A
13 < 13 是 False,所以 13 被错误地排除了:这正是边界测试能发现的那种 bug。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出哪个测试能抓住它?
这个函数应该把 18 岁及以上的人判定为成年人。哪一个测试能抓住这个 bug?
- 改需求最后一块
chunk(items, size)把列表切成每块size个。它把最后那块较短的弄丢了:chunk([1, 2, 3, 4, 5], 2)应该是[[1, 2], [3, 4], [5]]。修好它,并确保测试里的边界情况也都正确。 - 从零写测试密码规则
password_ok(pw)接受 8 到 64 个字符、至少有一个数字和一个字母的密码。写几个test_函数,让它们在它上面通过,并抓住全部四个坏版本(还有一个隐藏的)。想想边界:哪些长度是关键?
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| Boundary value | 边界值 | An input at or just past a limit. |
| Edge case | 边界情况 | An unusual input: empty, zero, the largest value… |
| Mutation testing | 变异测试 | Testing your tests by planting small bugs. |