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

TDD 与回归测试 · 软件测试

什么时候写测试最好?

课程目录

为什么需要它

先写测试听起来像是反着来,但这是编程中最有用的习惯之一。它逼你在写代码之前先想清楚代码该做什么,证明这个测试确实会失败,还会留下一道防线,让同样的 bug 再也回不来。

直觉理解

测试驱动开发(TDD)是一个短小的循环:

  1. 红:为下一个小行为写一个测试。运行它:失败(代码还没有这个功能)。
  2. 绿:写出能让它通过的最简单的代码。
  3. 重构:在所有测试保持绿色的前提下整理代码。

修 bug 也是同样的做法。 动代码之前,先写一个能复现 bug 报告的测试。看它失败:这样你就知道自己真的理解了这个 bug。修代码,看它通过。这个测试永远保留:它是一个回归测试,如果有人把 bug 带回来,它就会失败。

读失败的测试要从下往上读:期望什么、实际返回了什么、输入是什么。通常这就足够找到原因了。

定义

测试驱动开发

Test-driven development: write a failing test, make it pass, then tidy up, in small steps.
测试驱动开发:写一个失败的测试,让它通过,再整理代码,一小步一小步来。

回归测试

A test written for a bug that was fixed, kept so the bug can't silently return.
为一个已修复的 bug 写的测试,保留下来,让这个 bug 无法悄悄回来。

示例

先复现,再修复

动手试试

bug 报告:word_count(" hello world ") 返回了 7。测试先失败,修好之后通过。

def word_count_old(text):
    return len(text.split(" "))

def word_count_new(text):
    return len(text.split())

def test_extra_spaces(word_count):
    got = word_count("  hello   world ")
    assert got == 2, f"expected 2, got {got}"

for version in [word_count_old, word_count_new]:
    try:
        test_extra_spaces(version)
        print(version.__name__, "PASS")
    except AssertionError as e:
        print(version.__name__, "FAIL:", e)
输出
word_count_old FAIL: expected 2, got 7
word_count_new PASS
逐步执行
  1. L2split(" ") 每遇到一个多余的空格就产生一个空字符串。
  2. L5不带参数的 split() 会跳过连续的空格。
  3. L7回归测试,照着 bug 报告写的。
  4. L11旧代码红,新代码绿。

红、绿、重构

动手试试

FizzBuzz 的三轮小 TDD,每轮由一个新测试推动。

def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)

TESTS = [(1, "1"), (3, "Fizz"), (5, "Buzz"), (15, "FizzBuzz"), (30, "FizzBuzz")]
for n, want in TESTS:
    assert fizzbuzz(n) == want, (n, fizzbuzz(n))
print("all", len(TESTS), "tests green")
输出
all 5 tests green
逐步执行
  1. L8第 1 轮:1 → "1" 的测试只需要这一行。
  2. L5第 2 轮:3 的测试加了这个分支。
  3. L3第 3 轮:15 返回了 "Fizz" 而失败,所以这个检查必须放在最前面。

常见错误

还没复现就去修

如果你从没看到测试失败,就不知道它到底有没有测到这个 bug。先红。

改测试来让它通过

测试失败时,先问是谁错了:代码还是测试。只有需求真的这么说时,才去改期望值。

步子太大

先写十个测试再写全部代码,就失去了意义。一个小测试,让它通过,重复。

小测验

1. 有人报告了一个 bug。你首先应该做什么?

  1. A写一个能复现它的测试,看它失败
  2. B改代码直到看起来修好了
  3. C删掉失败的测试
查看答案

答案:A

一个失败的测试先证明你理解了 bug,再证明修复有效,最后永远守着它。

2. 在 TDD 里,写完一个失败的测试之后,紧接着做什么?

  1. A写出能让它通过的最简单的代码
  2. B把所有代码都重构一遍
  3. C再写十个测试
查看答案

答案:A

红 → 绿 → 重构,一小步一小步来。

动手练习

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

  1. 预测输出哪些测试失败?

    准确写出它打印的内容。

  2. 改需求修复 bug #42

    bug #42 的回归测试已经写好了,现在是失败的。修改 parse_price 让它通过,还要处理文字两头的空格(" $12,000 " → 12000.0)。不要改测试。

  3. 从零写让测试变绿

    测试先写好了。写一个 to_roman(n),支持 1 到 3999,让它们全部通过:

    1 → I   4 → IV   9 → IX   14 → XIV   40 → XL   90 → XC
    400 → CD   1994 → MCMXCIV   2026 → MMXXVI   3999 → MMMCMXCIX

在互动课里练这一课

打开互动课,这些例子都能直接改、直接运行,练习题提交马上判题。免费开始,不用注册。

互动课按顺序解锁:前面的课还没学,会先带你到课程地图。

本课术语

术语中文英文定义
TDD测试驱动开发Red, green, refactor: write the test first.
Regression test回归测试A test kept so a fixed bug can't come back.