4. pytest 框架快速入门归纳总结
如果你在团队里喊一声「谁用过 pytest?」,我猜至少一半人会举手。说实话,Python 的测试框架不少,但 pytest 能成为主流,靠的就是「简单到不需要学」和「强大到够你用很久」。我自己从 unittest 切到 pytest 后,再也没回头。
这一章我们不讲长篇大论,只把最核心的东西捋一遍。你跟着操作下来,应该就能直接上手写测试了。
4.1 为什么你应该用 pytest 而不是 unittest?
先聊个我自己的经历。最早我写测试用的是 unittest,总觉得别扭——每个类都要继承,每套 case 都得写 setup/teardown,跑个参数化还得自己拼循环。直到有一次项目需要测 200 个 API 接口的返回值,unittest 那套写法让我改到崩溃。换 pytest 后,代码从 800 行缩到 200 行,运行速度还快了 30%。
说白了,pytest 的核心优势就三个:
- 零模板代码——不需要继承,不需要写类名,函数名带 test_ 就行。
- 断言天然友好——直接用 Python 的 assert 关键字,不用记一堆 assertEqual、assertTrue。
- fixture 取代 setup/teardown——资源管理灵活到令人发指。
举个例子,unittest 里这样写:
import unittest
class TestStringMethods(unittest.TestCase):
def setUp(self):
self.data = "hello"
def test_upper(self):
self.assertEqual(self.data.upper(), "HELLO")
if __name__ == '__main__':
unittest.main()
pytest 里同一件事:
def test_upper():
data = "hello"
assert data.upper() == "HELLO"
哪个更清爽?我不说你也知道。
4.2 十分钟搭好你的第一个 pytest 环境
安装就一句话,我建议用 pip 装:
pip install pytest
然后写个最简单的测试文件 test_demo.py:
def test_add():
assert 1 + 1 == 2
def test_sub():
assert 2 - 1 == 1
终端里敲 pytest,你会看到类似这样的输出:
============================= test session starts =============================
platform linux -- Python 3.10.0, pytest-7.4.0, pluggy-1.0.0
collected 2 items
test_demo.py .. [100%]
============================== 2 passed in 0.02s =============================
两个点代表两个测试通过了,零配置。嗯,就这么简单。
test_*.py 或 *_test.py 文件,然后执行里边所有 test_ 开头的函数。这比 unittest 手动加载 TestCase 方便太多了。
4.3 掌握了这几种断言,你就够用了
pytest 的断言直觉到什么程度?你用 assert 后面跟任何条件就行。如果失败了,pytest 会智能地把表达式拆开给你看。
| 场景 | 写法 | 失败时你看到什么 |
|---|---|---|
| 等于 | assert a == b |
显示 a 和 b 的具体值 |
| 包含 | assert "hello" in "hello world" |
显示 in 两边的字符串 |
| 真值 | assert result is True |
显示 result 的实际值 |
| 异常 | with pytest.raises(ValueError): func() |
显示是否未被捕获的异常类型 |
| 近似相等 | assert a == pytest.approx(b, rel=1e-3) |
显示 a、b 以及相对误差 |
注意最后那个 pytest.approx,我在测金融计算的时候经常用它。浮点数直接比较很容易踩坑,不信你试试 0.1 + 0.2 == 0.3 的结果。
assert something 而不理失败细节。我之前有个同事在测试里只写了 assert response.status_code == 200,结果接口返回了 500 他也只看得到空报错,查了半天才发现是断言没写消息。pytest 虽然会帮你看表达式,但如果你有判断逻辑复合条件,建议加个注释或用 pytest.fail() 显式说明。
4.4 Fixture:测试的资源管理器
fixture 是 pytest 最灵魂的设计,没有之一。你想在测试前创建数据库连接、启动一个服务、读一个文件——统统交给 fixture 去管。
简单例子:
import pytest
@pytest.fixture
def user_data():
return {"name": "Alice", "age": 30}
def test_user_name(user_data):
assert user_data["name"] == "Alice"
def test_user_age(user_data):
assert user_data["age"] == 30
你看到 user_data 这个 fixture 被两个测试用了。pytest 会自动把这个 fixture 的结果传给测试函数的同名参数。我不需要显式调用,也不需要关心清理。
fixture 还可以带 scope 参数:
function(默认):每个测试函数独享一份资源class:每个测试类共享一份module:每个测试文件共享一份session:整个测试会话共享一份
举个例子,我有一个慢得要死的模型加载操作:
@pytest.fixture(scope="session")
def big_model():
print("\n加载大模型...")
return load_model()
这样整个测试过程只会加载一次,后面所有测试函数都复用这个模型实例。我在一次回归测试中靠这个把执行时间从 15 分钟降到了 2 分钟。
4.5 参数化:一行变多行
如果你有一组输入输出对要测,不用写一堆重复函数。pytest 的 @pytest.mark.parametrize 帮你做到:
import pytest
@pytest.mark.parametrize("input_val, expected", [
(1, 2),
(2, 4),
(10, 20),
(0, 0),
])
def test_double(input_val, expected):
assert input_val * 2 == expected
你运行一下,会看到 4 个测试用例单独显示,报错时也能精确知道是哪组数据挂了。
我通常用一个列表把测试集放在外面,方便维护:
test_cases = [
("2024-01-01", True),
("2024-13-01", False),
("not a date", False),
]
@pytest.mark.parametrize("date_str, is_valid", test_cases)
def test_date_validation(date_str, is_valid):
assert is_date_valid(date_str) == is_valid
4.6 测试执行与报告:pytest 的日常操作
到现在为止,你只需要记住几个常用参数:
| 命令 | 作用 |
|---|---|
pytest |
递归跑所有 test_ 文件 |
pytest test_api.py |
只跑指定文件 |
pytest test_api.py::test_login |
只跑文件里的某一个测试函数 |
pytest -k "login" |
用关键字匹配 test_login、test_user_login 等 |
pytest -v |
详细显示每个测试的名称和结果 |
pytest -s |
允许输出 print() 到控制台(默认会被捕获) |
pytest --tb=short |
显示简短的异常回溯 |
pytest --cov=my_module |
配合 pytest-cov 插件输出覆盖率报告 |
我个人最常用的是 pytest -v -k "smoke",用来快速验证核心冒烟用例。曾经有一次线上事故,就是因为我把一个冒烟测试放到慢测试组里了,跑一整个套件等半小时才发现问题。从那以后我学乖了,用标记 + -k 把冒烟和全量分开。
核心流程总结:pytest 按照「发现测试→收集测试→执行测试→生成报告」的顺序工作。下图把这一流程拆成了 6 个关键阶段,你可以对照着理解每步发生了什么。
4.7 一点儿关于插件和标记
pytest 生态里有很多好用的小插件。我安装完 pytest 后,还会随手装:
pytest-html—— 生成漂亮的 HTML 报告pytest-xdist—— 并行跑测试,节省时间pytest-cov—— 查看代码覆盖率pytest-ordering—— 控制测试的执行顺序
标记(mark)是用来给测试打标签的。例如把某些测试标记为 smoke:
@pytest.mark.smoke
def test_login_success():
assert login("admin", "123456") == True
然后执行 pytest -m smoke 就只跑冒烟用例。你也可以用 @pytest.mark.skip 跳过某个测试,或者用 @pytest.mark.xfail 标记已知的失败。
4.8 一个完整的快速上手示例
把上面所有东西揉在一起,应该是这样的:
# test_calc.py
import pytest
# fixture 提供测试数据
@pytest.fixture
def calculator():
return lambda a, b: a + b
# 参数化测试多组数据
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 5, 4),
])
def test_add(calculator, a, b, expected):
assert calculator(a, b) == expected
@pytest.mark.smoke
def test_add_positive():
assert 2 + 3 == 5
保存后 pytest -v 运行,你会看到 4 个测试(3 个参数化 + 1 个冒烟)全部通过。你的第一个自动化测试集成搞定。
嗯,到现在为止,你已经能应付日常 80% 的测试场景了。剩下的内容——比如 conftest.py、钩子函数、自定义命令行参数——后面章节会逐一展开,但那都是锦上添花的事了。先把这些基础操练熟,跑起来再说。