TDD 与回归测试 · 软件测试
什么时候写测试最好?
课程目录为什么需要它
先写测试听起来像是反着来,但这是编程中最有用的习惯之一。它逼你在写代码之前先想清楚代码该做什么,证明这个测试确实会失败,还会留下一道防线,让同样的 bug 再也回不来。
直觉理解
测试驱动开发(TDD)是一个短小的循环:
- 红:为下一个小行为写一个测试。运行它:失败(代码还没有这个功能)。
- 绿:写出能让它通过的最简单的代码。
- 重构:在所有测试保持绿色的前提下整理代码。
修 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
逐步执行
- L2split(" ") 每遇到一个多余的空格就产生一个空字符串。
- L5不带参数的 split() 会跳过连续的空格。
- L7回归测试,照着 bug 报告写的。
- 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
逐步执行
- L8第 1 轮:1 → "1" 的测试只需要这一行。
- L5第 2 轮:3 的测试加了这个分支。
- L3第 3 轮:15 返回了 "Fizz" 而失败,所以这个检查必须放在最前面。
常见错误
还没复现就去修
如果你从没看到测试失败,就不知道它到底有没有测到这个 bug。先红。
改测试来让它通过
测试失败时,先问是谁错了:代码还是测试。只有需求真的这么说时,才去改期望值。
步子太大
先写十个测试再写全部代码,就失去了意义。一个小测试,让它通过,重复。
小测验
1. 有人报告了一个 bug。你首先应该做什么?
查看答案
答案:A
一个失败的测试先证明你理解了 bug,再证明修复有效,最后永远守着它。
2. 在 TDD 里,写完一个失败的测试之后,紧接着做什么?
查看答案
答案:A
红 → 绿 → 重构,一小步一小步来。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出哪些测试失败?
准确写出它打印的内容。
- 改需求修复 bug #42
bug #42 的回归测试已经写好了,现在是失败的。修改
parse_price让它通过,还要处理文字两头的空格(" $12,000 "→12000.0)。不要改测试。 - 从零写让测试变绿
测试先写好了。写一个
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. |