






















今天在 GitHub Trending 上看到一个经典项目再次上榜:pytest,Python 生态中拥有 58k+ Star 的测试框架,几乎每个 Python 开发者都会接触。但它远不止 assert 那么简单,今天就来深入看看它的设计精髓。
pytest 是 Python 生态中最流行的测试框架,由 Holger Krekel 于 2004 年创建,采用 MIT 许可证开源。它让编写小测试变得极其简单,同时又能扩展支持复杂的应用和库级别的功能测试。
与 Python 标准库自带的 unittest 相比,pytest 的核心理念是:让测试代码像普通 Python 代码一样自然。你不需要继承 TestCase 类,不需要记住 self.assertEqual、self.assertTrue 这些方法名,只需要用最朴素的 assert 语句即可。
核心特性包括:
assert 语句会自动显示变量值,无需手动编写错误信息@pytest.fixture 装饰器管理测试资源,支持作用域和参数化unittest 和 trial 测试套件pytest 的架构采用经典的 插件化管道 设计,整个测试执行过程分为多个阶段,每个阶段都可以通过 hook 函数进行扩展:
命令行解析 → 配置加载 → 测试收集 → 测试选择 → 测试执行 → 报告输出
核心模块位于 src/_pytest/ 目录,关键模块包括:
config.py:命令行参数解析与配置管理(_console_main 入口)collection.py:测试发现与收集,递归遍历文件系统runner.py:测试执行引擎,管理 fixture 生命周期fixtures.py:Fixture 解析与依赖注入系统assertion.py:断言重写(通过 AST 操作,在 assert 语句中插入诊断代码)pytest 最令人印象深刻的技术之一是 assertion rewriting。在 import 阶段,pytest 会通过 import hook 拦截测试模块,将其 AST(抽象语法树)中的 assert 语句改写为包含详细诊断信息的代码:
# 你写的代码
def test_answer():
assert inc(3) == 5
# pytest 内部改写后大致等价于
def test_answer():
if not (result := inc(3)) == 5:
raise AssertionError(f"assert {result!r} == 5\n where {result} = inc(3)")
这就是为什么 pytest 能在断言失败时显示清晰的变量值,而不需要你写任何额外的断言方法。这一机制在 src/_pytest/assertion/rewriting.py 中实现。
pytest 的 Fixture 系统借鉴了依赖注入的思想。每个 fixture 是一个返回值的函数,通过函数参数声明依赖关系:
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn
conn.close()
def test_query(db_connection):
result = db_connection.execute("SELECT 1")
assert result.fetchone()[0] == 1
Fixture 支持四种作用域:function、class、module、package、session。通过 yield 语句实现资源清理,确保测试结束后正确释放资源。
从 pyproject.toml 中可以看到,pytest 自身的开发也大量使用了 fixture:
[tool.pytest]
minversion = "2.0"
addopts = ["-rfEX", "-p", "pytester"]
其中 -p pytester 加载了内置的 pytester 插件,专门用于测试 pytest 自身。
pytest 的插件体系基于 Pluggy 库(pluggy>=1.5,<2),这是一个通用的 hook 管理库。插件通过实现特定的 hook 方法来介入测试生命周期的各个阶段:
# hookimpl 标记一个方法为 hook
@hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
"""在测试收集完成后、执行前修改测试顺序"""
items.sort(key=lambda x: x.name)
从依赖来看,pytest 的核心依赖非常精简:
pluggy:Hook/插件系统iniconfig:配置文件解析(pytest.ini)packaging:版本号比较pygments:终端彩色输出pytest 自身的工程质量堪称教科书级别。从 pyproject.toml 可以看到:
mypy(strict 模式)和 pyright,check_untyped_defs = true,disallow_untyped_defs = truecoverage.py,启用 branch = true,覆盖率可通过 CI badge 查看pre-commit.ci# 基础安装
pip install pytest
# 可选:安装开发依赖
pip install pytest[dev]
创建一个 test_sample.py 文件:
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 4
运行测试:
$ pytest
============================= test session starts =============================
collected 1 items
test_sample.py . [100%]
============================== 1 passed in 0.04s ==============================
只需 assert 语句,无需继承任何类,无需调用任何 runner——这就是 pytest 的优雅之处。
pytest 的 @pytest.mark.parametrize 装饰器让你用一组数据驱动同一个测试:
@pytest.mark.parametrize("input,expected", [
("3+5", 8),
("2+4", 6),
("6*9", 42),
])
def test_eval(input, expected):
assert eval(input) == expected
@pytest.fixture(scope="session")
def redis_client():
client = redis.Redis()
client.flushall() # 测试前清空
yield client
client.close()
@pytest.fixture
def cached_data(redis_client):
# 依赖另一个 fixture
return redis_client.get("test_key")
@pytest.mark.slow
def test_large_dataset():
...
@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
def test_linux_feature():
...
运行时按标记筛选:
pytest -m "not slow" # 跳过慢测试
pytest -k "test_login" # 按名称匹配
在项目目录下创建 conftest.py,其中的 fixture 会被该目录及子目录的所有测试自动发现和使用,无需 import。这是 pytest 实现测试资源共享的核心机制。
pytest 命令找不到?确保安装时 Python 环境正确激活。在虚拟环境中安装后,需要激活该虚拟环境:
source venv/bin/activate
which pytest # 应显示虚拟环境路径
检查 conftest.py 是否放在正确的目录层级中。fixture 的可见范围取决于 conftest.py 的位置——放在项目根目录的 conftest.py 对所有测试可见。
pytest 默认收集以 test_ 开头或 _test 结尾的文件,以及以 Test 开头的类中以 test_ 开头的方法。如果你的文件不符合命名规则,可以通过 pyproject.toml 自定义:
[tool.pytest]
python_files = ["test_*.py", "check_*.py"]
安装 pytest-asyncio 插件:
pip install pytest-asyncio
@pytest.mark.asyncio
async def test_async_operation():
result = await async_func()
assert result is not None
pytest 可以直接运行 unittest 测试用例,无需任何修改。但如果你想在 unittest 风格的测试中使用 pytest 的 fixture,可以通过 pytestmark 或混合继承的方式实现。
pytest 之所以能成为 Python 测试的事实标准,不仅因为它让写测试变简单了,更在于它深厚的工程设计:
assert 拥有媲美 self.assert* 的诊断能力如果你还在用 unittest 写测试,不妨给 pytest 一个机会——从把 self.assertEqual 改成 assert 开始,你会发现测试也可以是一件优雅的事。
📦 项目地址:github.com/pytest-dev/pytest 📖 官方文档:docs.pytest.org
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。