pytest 是 Python 生态中最主流的测试框架。它比 unittest 更简洁、更强大,大量项目(包括 Django、Flask 本身)都用它来写测试。核心体验可以概括为:用极少量的代码写出易读、好维护的测试。
用例编写
- 自动发现:pytest 会自动查找以
test_开头的文件和函数,以及以Test开头的类中的test_方法。你只要按规范命名,无需注册。 - 测试函数即用例:最简单的测试就是一个普通函数,用
assert断言预期结果。
# test_calc.py
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
运行 pytest 命令,它自动发现并执行,输出清晰的成功/失败信息。
- 可读性好:测试逻辑就是普通的 Python 代码,没有多余的样板。团队成员看测试就能知道被测代码的预期行为。
断言
pytest 抛弃了 unittest 中 self.assertEqual() 这类繁琐的断言方法,直接使用 Python 内置的 assert 语句。
- 优势:当断言失败时,pytest 会智能地展示表达式的值和差异,帮助快速定位问题。例如:
def test_dict():
result = {"a": 1, "b": 2}
assert result == {"a": 1, "b": 3}
会给出清晰的对比,告诉你哪个键值不一致。
- 异常断言:用
pytest.raises上下文管理器来验证代码是否按预期抛出异常:
import pytest
def test_division_by_zero():
with pytest.raises(ZeroDivisionError):
1 / 0
- 警告断言:也可用
pytest.warns验证警告信息的产生。
固件(Fixtures)
固件是 pytest 中管理测试前置资源(如数据库连接、临时文件、测试数据)的核心机制。
- 定义与注入:用
@pytest.fixture定义一个固件函数,测试函数通过参数名引用它,pytest 会自动注入。
import pytest
@pytest.fixture
def sample_data():
return {"name": "Alice", "age": 30}
def test_user_age(sample_data):
assert sample_data["age"] == 30
- 生命周期管理:固件可以有设置和清理逻辑。使用
yield代替return,yield之前的代码是设置,之后的代码是清理(如关闭文件、断开数据库连接)。即便测试失败,清理也会执行。
@pytest.fixture
def temp_file():
f = open("temp.txt", "w")
yield f
f.close()
- 作用域控制:通过
scope参数指定固件的复用范围(function、class、module、session),避免重复创建开销大的资源。 - 实际价值:固件让测试依赖变得显式、可复用,不再需要在每个测试里复制粘贴初始化代码。测试代码更干净,也更易维护。
参数化
当一个测试逻辑需要对多组输入输出进行验证时,pytest 的 @pytest.mark.parametrize 装饰器可以成倍减少重复代码。
- 基本用法:传入参数名和一组值,pytest 会为每个组合生成一个独立的测试用例。
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expected
这样一次运行会得到三个测试结果,失败时会明确告诉你是哪组数据出了问题。
- 组合参数化:可以对多个参数独立参数化,pytest 会生成所有可能的组合。
- 与固件配合:参数化的值也可以来自固件,实现更灵活的数据驱动测试。
- 实际好处:用数据表驱动测试覆盖多种场景,代码量少,覆盖度高,新增测试用例只需加一行数据。
这四项能力构成了 pytest 日常使用的基础。再加上丰富的插件生态(如 pytest-cov 统计覆盖率、pytest-mock 增强模拟、pytest-django 集成 Django),pytest 可以优雅地支撑从小脚本到大型项目的测试需要。