前置知识: 零基础起步

测试基础:让机器替你验证代码

9 min入门

从一个会算错的 is_even 函数开始,写出你人生第一组自动化测试:断言、测试用例三要素、回归测试的真正含义,附真实 AssertionError 调试实录与梯度练习。

前置知识

  • 会用 Python 跑脚本:能运行 python 文件名.py 并看懂输出(第二门语言体验 级别即可);
  • 不需要任何测试框架,本文只用 Python 自带的 assert 语句。

学习目标

读完本文你将能够:

  1. 说出「测试」在工程里的准确定义:用代码描述期望行为,让机器替你反复验证;
  2. 写出第一组自动化测试,并亲眼看到它抓住一个真实 bug;
  3. 用「输入、执行、断言」三要素拆解任何一个测试用例;
  4. 解释为什么「修好 bug 后补一条测试」是程序员最重要的自保习惯;
  5. 识别「永远通过的假测试」这类新手陷阱。

预计 45 到 60 分钟。

1. 你现在要解决什么问题

你写了一个判断偶数的函数,交给同事用。第二天有人反馈:「输入 -2 它说是奇数」。你打开代码看了看,改好了,发回去。三天后又有反馈:「2000 年是不是闰年你们算错了」——另一个函数的另一个边界。

问题不在你改得慢,而在验证方式:每次都是「人手点一遍」。改一处、点一遍,改十处、点十遍,漏点一遍就上线一个 bug。更糟的是,上个月修好的问题,这个月改别处时又悄悄弄坏,没有任何东西提醒你。

你需要的是:把「应该怎样」写成代码,让机器在你每次改完代码后自动检查一遍。这就是自动化测试。

2. 先不要看解释,先试试看

新建目录 test-lab,放入被测函数文件 even.py:

python
def is_even(n):
    return n % 2 == 1   # 注意:这里有一个真 bug,先别改,往下走

再写一个验证脚本 verify_even.py:

python
from even import is_even

assert is_even(4), "4 应该是偶数"
assert is_even(0), "0 应该是偶数"
assert is_even(-2), "-2 应该是偶数"
assert not is_even(7), "7 不应该被判定为偶数"

print("全部通过")

先预测:这个脚本能跑到最后打出「全部通过」吗?如果会报错,报在第几行、错误信息是什么?把预测写下来,然后运行:

bash
python verify_even.py

3. 最小可运行示例:测试抓住 bug 的现场

预期输出(真实文本):

text
Traceback (most recent call last):
  File "verify_even.py", line 3, in <module>
    assert is_even(4), "4 应该是偶数"
AssertionError: 4 应该是偶数

第一条测试就挂了。恭喜——你的测试在你用户之前抓住了 bug:n % 2 == 1 判断的其实是「是不是奇数」。

现在修 bug(把 even.py 的判断改为 n % 2 == 0),重新运行:

text
全部通过

四条验证全部变绿。这两次运行合起来,就是本文要教的一切的浓缩:把期望写成断言,机器替你发现「现实与期望不符」。

4. 发生了什么

  • assert 条件, 消息 是 Python 原生语句:条件为真,什么都不发生,脚本继续走;条件为假,立刻抛出 AssertionError 并显示你的消息;
  • 断言遇错即停:第一条失败后,后面的测试根本没跑——这就是为什么你只看到一条报错(这个特性第 9 节会变成练习);
  • 修完 bug 重跑,脚本安静地打印「全部通过」。测试的价值不只在于抓错,更在于「绿」给你的底气:这四个行为你随时可以证明,而不是靠记忆。

读报错三步同样适用:line 3 告诉你哪条测试失败,assert is_even(4) 告诉你哪个输入出的问题,4 应该是偶数 是你自己写给未来的自己的说明。好测试失败的瞬间,问题已经定位了一半。

5. 核心概念

测试用例三要素:任何一个测试都能拆成三段——

要素本文示例说明
准备输入调用 is_even(4)给定一个具体场景
执行函数真的跑一遍触发被测行为
断言assert 结果为真把「期望」写死在代码里

缺任何一环都不算测试。最常见的残缺是「有执行没断言」——代码跑了、没报错,就以为对了,实际上什么都没验证(第 7 节实录)。

回归测试:第 1 节那个「上个月修好的问题这个月又坏了」的场景,术语叫回归。自动化测试存在的最大理由就是防回归:测试写好一次,之后每次改动代码都可以全部重跑,机器不嫌烦。修 bug 的标准姿势因此变成两步:先写一条能复现 bug 的测试(此刻它应该变红),再修代码让它变绿,测试从此留下,永久守护这个边界。

好测试的三个标准:快(毫秒级,才敢频繁跑);可重复(不依赖网络、随机、日期——跑一万次结果一致);失败时信息充分(断言消息写「-2 应该是偶数」而不是「出错了」)。

测试金字塔(概念预告):单元测试(本文这种,测一个函数)量最大、跑最快;往上是对几个模块协作的集成测试;最顶上是对整个系统界面操作的端到端测试。比例大致是「多、中、少」,展开在 测试级别。

6. 修改实验

  1. 给 verify_even.py 增加一行 assert is_even(1000000), "大偶数也应通过",重跑——绿了?把你 even.py 改回 bug 版,验证测试确实会红,再修回来。亲手体验「测试在守护你」;
  2. 把第一条断言的消息删掉,改回 bug 版运行,对比报错信息——体会「失败时信息充分」这条标准值多少钱;
  3. 新增一个断言 assert is_even(2.0), "2.0 是偶数",先预测(提示:2.0 % 2 == 0 吗?),再运行。发现自己对函数行为了解不全,就是测试的目的达成。

7. 调试实录:永远通过的假测试

同事说他写了测试,全绿。代码如下:

python
def test_add():
    result = 2 + 3
    print("计算完成")

这段「测试」永远通过——它只执行、不断言,print 不检查任何东西。没有断言的测试等于没有测试,甚至更糟:它给你虚假的安全感。修复:

python
def test_add():
    result = 2 + 3
    assert result == 5, "2 + 3 应该等于 5"

第二个真实陷阱方向相反:改测试凑合代码。测试红了,第一反应应该是「代码错了还是测试错了」,两种情况都有可能,但必须查明原因再动笔。直接把期望值改成代码的实际输出让它变绿,等于把「验证」偷换成「记录现状」——红灯是测试唯一的价值,亲手拧灭它不如删掉测试。

第三个陷阱:把一堆断言塞进一个巨大测试函数。失败时只能看到第一个错,后面的全被跳过。按「一个测试函数验证一个行为」拆开,失败信息才清晰(这个组织工作就是 pytest 的用武之地)。

附注一个准确性细节:Python 解释器加 -O 参数运行时会跳过所有 assert,所以 assert 适合本文的教学与内部脚本;正式项目的测试用 pytest 等框架的断言机制,不受此影响。

8. 实际项目中的使用场景

  • 代码仓库里通常有一个 tests/ 目录与被测代码平行,CI 流水线在每次提交时自动跑全部测试,红了就拦下合并——你以后在 GitHub 上看到的绿色对勾就是它;
  • 修任何 bug 之前先写复现测试,是团队里公认的靠谱信号;
  • 重构(不改行为、只改结构)敢不敢大刀阔斧,完全取决于有没有一套可信的测试——这就是「重构的勇气是测试给的」。

9. 小练习

预测题:下面的脚本会打印什么?先写答案再运行:

python
assert 1 + 1 == 2, "第一条"
assert 2 * 2 == 5, "第二条"
assert 10 / 2 == 5, "第三条"
print("全对")

修改题:给 verify_even.py 补上一组负奇数与零的断言,并让每个断言的消息都具体到「输入是什么、期望是什么」。

修 Bug 题:下面的「测试」永远通过,指出两处问题并重写:一处在函数内部,一处在调用方式上——

python
def test_is_even():
    is_even(6)
    print("done")

test_is_even()

挑战题(不看提示自己写):新函数登场,leap.py 里是同事写的闰年判断:

python
def is_leap(year):
    return year % 4 == 0

先写一组断言暴露问题:至少覆盖 2024、2023、2000、1900 四个年份(其中有两个会打脸)。运行确认测试变红后,把函数修成完整规则(能被 4 整除且不能被 100 整除,或能被 400 整除),让测试变绿。最后回答:如果 1900 这个用例一开始就不写,bug 会在什么时候被谁发现?

10. 什么时候应该 / 不应该写测试

应该:任何有维护价值的逻辑(业务规则、工具函数、修过的 bug);团队项目的一切核心路径;你打算以后重构的代码(测试先行的安全网)。

不应该:一次性探索脚本、跑完就扔的数据处理;还没想清楚要什么的 spike 原型(等方向定了再补);界面「好看不好看」这类主观判断(视觉回归是另一个专门话题)。

11. 与之前和之后的知识的关系

  • 往前:你只需要会跑 Python——测试入门的门槛就这么低;
  • 往后:测试概念与原则 把「缺陷、验证、确认」这些术语与本文的实践对上号;测试级别 展开金字塔;等价类划分 回答「该挑哪些输入当用例」(本文的 2000 与 1900 就是边界值思路的第一次亮相);pytest 把今天手写的 assert 脚本升级成可自动发现、可分组、可出报告的正式用例。

12. 官方文档

13. 自我检查

  • 能不看示例默写「一个函数文件 + 一个验证脚本」并跑通红绿两个状态;
  • 能用三要素拆解任何一条测试;
  • 能解释「修 bug 先写复现测试」的完整流程与价值;
  • 能识别无断言假测试、改测试凑代码、巨型测试函数三个陷阱。

本章总结

测试不是「点一遍看看」,而是把期望写成机器可执行的断言:三要素(输入、执行、断言)构成用例,回归价值靠「测试留下、每次重跑」兑现,可信度靠「失败信息充分」支撑。你今天用四行 assert 体验了测试的全部核心——之后的框架、级别、方法论,都是在这四行上长出来的。

下一步

进入 测试概念与原则,把实践背后的术语体系补齐。