你的第一批测试 · 软件测试
改完代码之后,怎么知道它还能用?
课程目录为什么需要它
每次修改都可能弄坏原来好好的东西,而且常常坏在离你改的那一行很远的地方。每次改完都手动点一遍应用,又慢又容易漏。自动化测试每次都在几秒内替你检查,有了它,团队才敢放心地修改庞大的代码库。
直觉理解
测试就是一个小函数:运行你的代码,然后用 assert 检查结果:
def test_add():
assert add(2, 3) == 5assert 条件 在条件为真时什么都不做,为假时抛出 AssertionError。可以加一条消息说明哪里错了:assert got == 5, f"got {got}"。
大多数测试分三步:准备(构造输入)、执行(调用代码)、断言(检查结果)。
测试运行器会找到所有名字以 test_ 开头的函数,逐个运行,报告哪些通过了。在真实项目里用的是 pytest:
$ pytest
test_shop.py ..F [100%]
FAILED test_shop.py::test_extra_spaces这里我们用一个十行的运行器,做同样的事。
好的测试会在代码出错时失败。不管怎样都能通过的测试,什么也没检查。
定义
assert 断言
A statement that raises AssertionError when its condition is false. Tests are built from asserts.条件为假时抛出 AssertionError 的语句。测试就是由一个个断言组成的。
测试运行器
A tool that finds test functions, runs them and reports passes and failures. pytest is the usual one for Python.找到测试函数、运行它们、报告通过和失败的工具。Python 常用的是 pytest。
回归
Something that used to work and broke after a change. Tests exist mainly to catch regressions.以前好用、改动之后坏掉的东西。测试主要就是用来抓回归的。
示例
assert 断言
动手试试为真时不出声,为假时报错。
def add(a, b):
return a + b
assert add(2, 3) == 5
print("first check passed")
try:
assert add(2, 2) == 5, "2 + 2 should not be 5"
except AssertionError as e:
print("failed:", e)first check passed failed: 2 + 2 should not be 5
- L4为真,所以什么都没发生,程序继续往下走。
- L8为假:抛出 AssertionError,带着我们的消息。
- L10这里捕获它只是为了展示消息;测试运行器会替你做这件事。
failed: 2 + 2 should not be 5
三个测试和一个运行器
动手试试每个测试检查一种行为;运行器把结果全部报告出来。
def slugify(title):
return title.lower().replace(" ", "-")
def test_simple():
got = slugify("Hello World")
assert got == "hello-world", f"got {got!r}"
def test_already_lower():
got = slugify("abc")
assert got == "abc", f"got {got!r}"
def test_extra_spaces():
got = slugify(" Hi there ")
assert got == "hi-there", f"got {got!r}"
def run(tests):
passed = 0
for test in tests:
try:
test()
print("PASS", test.__name__)
passed += 1
except AssertionError as e:
print("FAIL", test.__name__, "-", e)
print(f"{passed}/{len(tests)} passed")
run([test_simple, test_already_lower, test_extra_spaces])PASS test_simple PASS test_already_lower FAIL test_extra_spaces - got '--hi--there-' 2/3 passed
- L1被测试的代码:把标题变成 URL 的一部分。
- L13一个棘手的输入:两头有空格,中间还是两个空格。
- L23一个测试失败不影响其他测试。
- L27两个通过。第三个显示了实际返回的值。
常见错误
不可能失败的测试
assert slugify("a") 只要结果非空就能通过。要和确切的期望值比较。
一个测试测太多
一个有十个断言的测试失败时,你不知道是哪个行为坏了。每个测试测一个行为,名字就说明是哪个。
只测正常情况
bug 藏在棘手的情况里:空输入、多余的空格、零、最大值。下一课就讲怎么找到它们。
小测验
1. total 是 10 时,`assert total == 10` 会做什么?
查看答案
答案:A
assert 只有在条件为假时才出声。
2. 哪个测试最有用?
查看答案
答案:A
只有精确比较才会在计算出错时失败。
动手练习
3 道练习,分三层:先预测程序的输出,再按新需求改程序,最后从零写一个。代码在浏览器里运行,提交马上判题。
- 预测输出读测试报告
准确写出它打印的内容。
- 改需求让测试通过
test_extra_spaces失败了。修改slugify(不是测试),让三个测试都通过:任何地方多余的空格都要去掉,单词之间只用一个短横线连接。 - 从零写能抓住 bug 的测试
discount(price, percent)按百分比打折,并四舍五入到分。下面有三个坏掉的版本。写几个test_函数,让它们在discount上通过,在每个坏版本上失败。passes(版本)用你的测试去跑某个版本;评测会用它跑真正的版本、三个坏版本,还有一个你看不到的坏版本。你现在这一个测试什么都抓不住。
本课术语
| 术语 | 中文 | 英文定义 |
|---|---|---|
| assert | assert 断言 | Raises AssertionError when its condition is false. |
| Unit test | 单元测试 | A small automated test of one piece of code. |
| pytest | pytest | The most common test runner for Python. |