第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断言体系 简单断言 • assert 原生语法 • 断言自省机制 • 自动拆解表达式 断言信息优化 • 自定义错误消息 • pytest.fail() 结构化输出 • -v / -s 调试参数 插件生态 • pytest-assume 软断言 • pytest.approx 浮点比较 • pytest-json-report 核心原则:用最少的代码,获取最多的信息

这张图把pytest断言分成了三个层次:最基础的简单断言、进阶的信息优化、以及扩展的插件生态。我个人建议你按这个顺序学习,先把基础打牢,再考虑优化和扩展。

最后说一句:断言不是越多越好。我见过有人一个测试函数写了30个断言,结果维护起来痛不欲生。好的断言策略是——关键路径全覆盖,边缘场景有代表,错误信息可追溯。做到这三点,你的回归测试就能真正「自动化」起来,而不是「自动化地报一堆看不懂的错」。

实战建议:刚开始用pytest断言时,先别急着上插件。用原生assert写一个月,等你对「什么时候该断言、断言什么内容」有了感觉,再引入软断言、浮点比较这些高级特性。循序渐进,才是正道。

可可精品资料库