人人都能学编程
English 开始学习
专项 · 软件测试第 2 课 / 共 5 课免费阅读

边界情况 · 软件测试

应该测试哪些输入?

课程目录

为什么需要它

你不可能测试所有可能的输入,而且大多数 bug 不在范围的中间。它们藏在边界上:空列表、正好等于上限、超出一点点、零。用心挑几个输入,比随便测一百个抓到的 bug 更多。

直觉理解

  1. 把输入分组,同一组的输入行为应该相同(合法 / 太短 / 太长;正数 / 零 / 负数)。每组通常一个测试就够了。
  2. 测试组和组之间的边界:如果规则是「8 到 64 个字符」,就测 7、8、64 和 65。差一错误(把 <= 写成 <)只会在这里暴露。
  3. 测试特殊值:空字符串或空列表、一个元素、零、负数、非常大的数、None。
  4. 也要测试非法输入。如果函数应该报错,就检查它确实报错了:
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
逐步执行
  1. L2规则说 90 分及以上是 A,但这里写的是高于 90。
  2. 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
逐步执行
  1. L6一个小工具:运行 fn,告诉你它有没有抛出那种错误。
  2. L13lambda 把调用推迟,好让 raises() 在 try 里面运行它。
  3. L140 是边界:它也应该被拒绝。
  4. L15合法金额:没有错误。False

常见错误

只测典型值

即使 90 那里有问题,50 和 95 也能通过。每个界限处和刚越过界限的地方都要放一个测试。

忘了空的情况

空字符串和空列表会导致崩溃(取不存在的第 0 个)和除以零。要专门测试它们。

测试报错却什么也没检查

try: f() except ValueError: pass 即使 f() 什么都没抛也能通过。最后要加 assert False(或者用 pytest.raises)。

小测验

1. 表单接受 13 到 120 岁。哪组测试值最好?

  1. A12、13、120、121
  2. B20、40、60、80
  3. C13、50、100
查看答案

答案:A

每个界限,以及越过每个界限一步的值。

2. 这段代码会输出什么?

def is_teen(age):
    return 13 < age < 20

print([age for age in [12, 13, 19, 20] if is_teen(age)])
  1. A
    [19]
  2. B
    [13, 19]
  3. C
    [13, 19, 20]
查看答案

答案:A

13 < 13 是 False,所以 13 被错误地排除了:这正是边界测试能发现的那种 bug。

动手练习

3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。

  1. 预测输出哪个测试能抓住它?

    这个函数应该把 18 岁及以上的人判定为成年人。哪一个测试能抓住这个 bug?

  2. 改需求最后一块

    chunk(items, size) 把列表切成每块 size 个。它把最后那块较短的弄丢了:chunk([1, 2, 3, 4, 5], 2) 应该是 [[1, 2], [3, 4], [5]]。修好它,并确保测试里的边界情况也都正确。

  3. 从零写测试密码规则

    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.