第4章:pytest断言——从入门到优雅
断言,说白了就是「拍胸脯保证」。你告诉程序:「这里的结果必须是xxx,如果不是,你就给我停下来报错」。在Python世界里,pytest的断言机制是我用过最顺手的,没有之一。
我记得刚转用pytest时,最大的感受就是——终于不用记那些assertEqual、assertTrue之类的破方法名了。一个assert搞定一切,多清爽。
4.1 简单断言:Python原生的力量
pytest的核心理念很朴素:直接用Python的assert语句。你不需要学任何新语法。
def test_add():
result = add(2, 3)
assert result == 5
def test_list():
items = [1, 2, 3]
assert 2 in items
assert len(items) == 3
def test_string():
name = "Hello World"
assert name.startswith("Hello")
assert "World" in name
你看,这就是最普通的Python代码。pytest在背后做了件聪明的事——它重写了assert语句,当断言失败时,能告诉你具体是哪两个值不相等。
实际运行效果:
FAILED test_demo.py::test_add - AssertionError: assert 5 == 6
+ where 5 = add(2, 3)
为什么会这样?pytest用了「断言自省」技术。它不光告诉你assert失败了,还帮你拆解出表达式里每个变量的值。我在项目中遇到过好几次,同事用unittest跑挂了,盯着屏幕看了半天不知道哪里不对。换成pytest后,错误信息一目了然。
4.2 断言失败信息优化:让错误更友好
默认的断言信息有时候还不够。比如你测一个复杂的API返回,光看「assert response.status_code == 200」失败了,你还要去翻日志看实际返回了什么。
我的习惯是:给关键断言加上描述信息。
def test_user_api():
response = get_user(42)
assert response.status_code == 200, \
f"获取用户接口失败,状态码: {response.status_code},响应体: {response.text[:200]}"
data = response.json()
assert data['name'] == '张三', \
f"用户名不匹配,期望: 张三,实际: {data.get('name')}"
嗯,这里要注意:描述信息不要太啰嗦,但关键数据一定要带上。我见过有人把整个响应体都塞进去,结果控制台刷了几千行……
我的小技巧:用pytest -v运行测试,能看到每个测试函数的详细结果。配合-s参数还能看到print输出,调试时特别好用。
还有一种情况:你希望断言失败时,能展示更结构化的信息。这时候可以用pytest的pytest.fail()方法:
def test_complex_validation():
errors = validate_data(input_data)
if errors:
pytest.fail(f"数据校验失败,发现 {len(errors)} 个错误:\n" +
"\n".join(f" - {e}" for e in errors))
我曾经在一个支付系统的测试中,用这种方式把校验失败的字段名和期望值都列出来。开发同学拿到报告后,直接定位问题,不用再来问我「到底哪里不对」了。
4.3 插件生态:断言能力的无限扩展
pytest的插件生态,说白了就是「你想要什么,基本都有」。我整理了几个跟断言相关的常用插件:
| 插件名 | 作用 | 安装命令 |
|---|---|---|
| pytest-assume | 软断言:一个断言失败后继续执行后续断言 | pip install pytest-assume |
| pytest-check | 类似assume,但语法更简洁 | pip install pytest-check |
| pytest-approx | 浮点数近似比较,解决精度问题 | 内置在pytest中 |
| pytest-json-report | 将断言结果输出为JSON格式 | pip install pytest-json-report |
我最常用的是pytest-assume。你想想看,有时候一个测试函数里有5个断言,第一个挂了后面4个就不跑了。你修完第一个,跑一下,第二个又挂了……反复提交代码,多烦人。
# 使用pytest-assume实现软断言
from pytest import assume
def test_user_profile():
user = get_user_profile(42)
with assume: assert user['name'] == '张三'
with assume: assert user['age'] == 25
with assume: assert user['email'] is not None
with assume: assert len(user['tags']) > 0
# 所有断言都会执行,最后汇总报告
避坑指南:我曾经在一个大型回归测试中全面使用软断言,结果发现测试报告变得又臭又长——一个测试用例报了20多个失败。后来我定了个规矩:关键路径用硬断言(失败即停),次要验证用软断言。这样既保证了效率,又不会信息过载。
再说说浮点数比较。做数据处理或科学计算的同学肯定遇到过:
def test_float():
result = 0.1 + 0.2
# 这行会失败!因为实际结果是 0.30000000000000004
# assert result == 0.3
# 正确做法:使用pytest.approx
assert result == pytest.approx(0.3, rel=1e-9)
pytest.approx支持相对误差和误差两种模式。我一般用相对误差,因为不同量级的数据用同一个误差不太合理。
知识体系总览
下面这张图是我自己梳理的pytest断言知识结构,你可以把它当作学习地图:
这张图把pytest断言分成了三个层次:最基础的简单断言、进阶的信息优化、以及扩展的插件生态。我个人建议你按这个顺序学习,先把基础打牢,再考虑优化和扩展。
最后说一句:断言不是越多越好。我见过有人一个测试函数写了30个断言,结果维护起来痛不欲生。好的断言策略是——关键路径全覆盖,边缘场景有代表,错误信息可追溯。做到这三点,你的回归测试就能真正「自动化」起来,而不是「自动化地报一堆看不懂的错」。
实战建议:刚开始用pytest断言时,先别急着上插件。用原生assert写一个月,等你对「什么时候该断言、断言什么内容」有了感觉,再引入软断言、浮点比较这些高级特性。循序渐进,才是正道。
可可精品资料库