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 =============================

两个点代表两个测试通过了,零配置。嗯,就这么简单。

小提示:默认情况下 pytest 会递归查找当前目录下所有 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 个关键阶段,你可以对照着理解每步发生了什么。

pytest 测试执行流程 1. 测试发现 递归搜索 test_*.py 2. 测试收集 识别 test_ 函数/类 3. 资源准备 fixture 初始化 4. 测试执行 运行每个测试函数 5. 断言与结果 通过/失败/错误 6. 报告生成 汇总、输出、中断判断 说明 流程从左到右、从上到下执行。 如果某个 fixture 初始化失败, 该测试直接标记为错误,跳过执行。

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、钩子函数、自定义命令行参数——后面章节会逐一展开,但那都是锦上添花的事了。先把这些基础操练熟,跑起来再说。