第四章 单元测试框架:unittest入门、pytest基础、测试用例编写规范
说实话,很多刚入行的同学对「单元测试」这件事有点抵触。觉得写测试比写代码还麻烦,浪费时间。我以前也这么想,直到有一次我重构了一个排序算法,改完发现结果全乱了——嗯,那次我花了整整一个下午手动验证,最后还漏了一个边界条件。从那以后,我再也不敢不写测试了。
这一章,我们就来聊聊 Python 里最常用的两个测试框架:unittest 和 pytest。说白了,它们就是帮你自动验证代码正确性的工具。你写一次测试,以后每次改代码都能一键跑完,省心省力。
4.1 unittest 入门:Python 自带的测试框架
unittest 是 Python 标准库的一部分,不需要额外安装。它的设计思路借鉴了 Java 的 JUnit,所以如果你写过 Java 测试,上手会非常快。
我个人习惯用 unittest 写一些比较正式的测试场景,比如算法验证、接口测试。它的结构很清晰:测试类、测试方法、断言方法,三件套。
4.1.1 基本用法
来看一个最简单的例子。假设我们要测试一个加法函数:
# calculator.py
def add(a, b):
return a + b
# test_calculator.py
import unittest
from calculator import add
class TestCalculator(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(1, 2), 3)
def test_add_negative(self):
self.assertEqual(add(-1, -2), -3)
def test_add_zero(self):
self.assertEqual(add(0, 0), 0)
if __name__ == '__main__':
unittest.main()
运行方式很简单:python test_calculator.py。你会看到类似这样的输出:
...
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
三个点代表三个测试都通过了。如果某个测试失败,会显示 F 并给出详细的错误信息。
test_ 开头,否则 unittest 不会识别。我刚开始写的时候经常忘记这个规则,结果测试跑了个寂寞。
4.1.2 常用断言方法
unittest 提供了很多断言方法,我列几个最常用的:
| 断言方法 | 作用 |
|---|---|
assertEqual(a, b) |
检查 a == b |
assertTrue(x) |
检查 x 为 True |
assertFalse(x) |
检查 x 为 False |
assertRaises(Error, func, args) |
检查是否抛出指定异常 |
assertAlmostEqual(a, b, places) |
检查浮点数近似相等(指定小数位) |
举个例子,验证一个除法函数:
def divide(a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b
class TestDivide(unittest.TestCase):
def test_divide_normal(self):
self.assertEqual(divide(10, 2), 5)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
这里用 assertRaises 来验证异常,非常实用。我曾经在写二分查找算法时,就靠这个捕获了数组越界的 bug。
4.2 pytest 基础:更简洁、更强大
pytest 是第三方库,需要安装:pip install pytest。它比 unittest 更简洁,写起来更像普通函数,不需要继承任何类。
我个人更喜欢用 pytest 做日常开发中的测试,因为它少写很多样板代码。你想想看,unittest 里每个测试都要写 self.assertEqual,pytest 直接用 Python 原生的 assert 就行。
4.2.1 基本用法
同样的加法测试,用 pytest 写是这样的:
# test_calculator_pytest.py
from calculator import add
def test_add_positive():
assert add(1, 2) == 3
def test_add_negative():
assert add(-1, -2) == -3
def test_add_zero():
assert add(0, 0) == 0
运行命令:pytest test_calculator_pytest.py。输出更简洁:
============================= test session starts ==============================
collected 3 items
test_calculator_pytest.py ... [100%]
============================== 3 passed in 0.02s ===============================
看到没?不需要写类,不需要写 unittest.main(),直接写函数就行。pytest 会自动发现所有以 test_ 开头的函数。
4.2.2 参数化测试
这是 pytest 的一个杀手锏。假设你要测试一个函数在不同输入下的表现,unittest 需要写多个方法,pytest 可以用 @pytest.mark.parametrize 一行搞定:
import pytest
def is_even(n):
return n % 2 == 0
@pytest.mark.parametrize("input, expected", [
(2, True),
(3, False),
(0, True),
(-2, True),
(-3, False),
])
def test_is_even(input, expected):
assert is_even(input) == expected
这样写,一个测试函数就覆盖了 5 个场景。如果某个场景失败,pytest 会精确告诉你哪个参数组合出了问题。我在验证排序算法时经常用这个特性,一次跑几十组数据,效率极高。
4.3 测试用例编写规范
写测试不是随便写写就行的。我见过不少同学写的测试,要么只测了 happy path,要么断言写得模棱两可。这里我总结几条实战经验。
4.3.1 命名规范
- 测试文件:
test_<模块名>.py或<模块名>_test.py - 测试函数/方法:
test_<被测试功能>_<场景> - 测试类(unittest):
Test<被测试类>
举个例子,如果你要测试 sort.py 里的 quick_sort 函数,测试文件叫 test_sort.py,测试函数叫 test_quick_sort_normal、test_quick_sort_empty 等。这样一看名字就知道在测什么。
4.3.2 测试覆盖原则
一个合格的测试用例,至少要覆盖这几类场景:
- 正常情况: 最常见的输入,期望正常输出
- 边界情况: 比如空列表、0、最大值、最小值
- 异常情况: 非法输入,期望抛出异常或返回错误码
- 特殊值: 比如 None、空字符串、负数
我曾经在验证一个二分查找算法时,只测了正常数组,结果漏掉了数组为空的情况。上线后用户一查空数组,程序直接崩溃。嗯,从那以后我写测试必加边界条件。
4.3.3 断言规范
- 一个测试只测一个逻辑点。 不要在一个测试函数里同时验证加法和除法。
- 断言要精确。 不要用
assert result这种模糊断言,要明确比较值。 - 测试之间要独立。 不要依赖其他测试的执行结果或顺序。
4.4 知识体系总览
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把 unittest、pytest 和编写规范三个部分串起来了。你可以看到,unittest 和 pytest 是两种不同的实现方式,但最终目标都是验证代码的正确性。而编写规范则是保证测试质量的关键。
4.5 实战建议
最后,给你几个实战中的小建议:
- 从 pytest 开始学。 它更简单,上手快,适合初学者。等你熟悉了测试思维,再回头看 unittest 也不迟。
- 每个算法函数至少写 3 个测试。 一个正常情况,一个边界情况,一个异常情况。这是最低标准。
- 测试代码也是代码。 同样需要维护、重构。不要写一堆重复的测试代码,学会用参数化、fixture 等技巧。
- 跑测试要养成习惯。 每次改完代码,顺手跑一下测试。我个人的习惯是:改代码前跑一次,改完后跑一次,确保没破坏原有功能。
好了,这一章的内容就到这里。unittest 和 pytest 都是非常成熟的框架,选哪个看你个人喜好。但不管用哪个,写测试这个习惯一定要养成。下一章我们会聊更高级的测试技巧,比如 mock 和 fixture,到时候再见。