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

你的第一批测试 · 软件测试

改完代码之后,怎么知道它还能用?

课程目录

为什么需要它

每次修改都可能弄坏原来好好的东西,而且常常坏在离你改的那一行很远的地方。每次改完都手动点一遍应用,又慢又容易漏。自动化测试每次都在几秒内替你检查,有了它,团队才敢放心地修改庞大的代码库。

直觉理解

测试就是一个小函数:运行你的代码,然后用 assert 检查结果:

def test_add():
    assert add(2, 3) == 5

assert 条件 在条件为真时什么都不做,为假时抛出 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
逐步执行
  1. L4为真,所以什么都没发生,程序继续往下走。
  2. L8为假:抛出 AssertionError,带着我们的消息。
  3. 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
逐步执行
  1. L1被测试的代码:把标题变成 URL 的一部分。
  2. L13一个棘手的输入:两头有空格,中间还是两个空格。
  3. L23一个测试失败不影响其他测试。
  4. L27两个通过。第三个显示了实际返回的值。

常见错误

不可能失败的测试

assert slugify("a") 只要结果非空就能通过。要和确切的期望值比较。

一个测试测太多

一个有十个断言的测试失败时,你不知道是哪个行为坏了。每个测试测一个行为,名字就说明是哪个。

只测正常情况

bug 藏在棘手的情况里:空输入、多余的空格、零、最大值。下一课就讲怎么找到它们。

小测验

1. total 是 10 时,`assert total == 10` 会做什么?

  1. A什么都不做;程序继续
  2. B打印 True
  3. C抛出 AssertionError
查看答案

答案:A

assert 只有在条件为假时才出声。

2. 哪个测试最有用?

  1. Aassert price_with_tax(100) == 120
  2. Bassert price_with_tax(100)
  3. Cassert price_with_tax(100) is not None
查看答案

答案:A

只有精确比较才会在计算出错时失败。

动手练习

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

  1. 预测输出读测试报告

    准确写出它打印的内容。

  2. 改需求让测试通过

    test_extra_spaces 失败了。修改 slugify(不是测试),让三个测试都通过:任何地方多余的空格都要去掉,单词之间只用一个短横线连接。

  3. 从零写能抓住 bug 的测试

    discount(price, percent) 按百分比打折,并四舍五入到分。下面有三个坏掉的版本。写几个 test_ 函数,让它们在 discount 上通过,在每个坏版本上失败。

    passes(版本) 用你的测试去跑某个版本;评测会用它跑真正的版本、三个坏版本,还有一个你看不到的坏版本。你现在这一个测试什么都抓不住。

在互动课里练这一课

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

本课术语

术语中文英文定义
assertassert 断言Raises AssertionError when its condition is false.
Unit test单元测试A small automated test of one piece of code.
pytestpytestThe most common test runner for Python.