Skip to content

第 12 章 · 插件体系与常用插件

本章目标:理解 pytest"装了就生效"的插件加载机制(entry-point、conftest.py、-p 三条通道),学会排查与禁用插件,并盘点测试工程中最值得安装的几个插件。

12.1 插件为什么"装了就生效"

pytest 的可扩展性建立在 pluggy 插件框架之上。任何 Python 包只要在打包元数据里注册了 pytest11 entry point,pip 安装后 pytest 启动时就会自动发现并加载它:

bash
pip install pytest-timeout    # 无需任何配置,立即生效

验证方式是查看扩展的测试头信息:

bash
$ pytest --trace-config
...
plugins: cov-7.x, xdist-3.x, timeout-2.x

除了 entry-point 自动加载,还有两条显式通道:

  1. pytest_plugins 变量:在根 conftest.py 中声明要加载的插件模块;
    python
    # content of conftest.py(仅限 root conftest,非根 conftest 使用已弃用)
    pytest_plugins = ("myapp.testsupport.myplugin",)
  2. -p NAME 命令行选项:启动期按名字加载插件模块或 entry point。

第 2 条通道加载的就是普通 Python 模块,这意味着你可以在仓库内维护私有插件。一个最小可用的本地插件长这样:

python
# content of myapp/testsupport/mini_plugin.py
def pytest_addoption(parser):
    # 注册自定义命令行选项
    parser.addoption("--env", default="test", help="目标环境")


def pytest_configure(config):
    # 启动期读取配置并打标
    config.option.env = config.getoption("--env")
python
# content of conftest.py —— 用 -p 加载上面的本地插件
pytest_plugins = ("myapp.testsupport.mini_plugin",)

之后运行 pytest -p myapp.testsupport.mini_plugin --env=staging 即可让整个套件感知目标环境。

名字冲突提醒

pytest_plugins 这个变量名被 pytest 保留,不要用它命名自己的插件模块。

12.2 插件的三个层级与禁用方法

pytest 把插件分为三层:内置插件(随主包发布,如 cacheprovider)、外部插件(entry-point 自动发现的 pip 包)、本地插件(各目录的 conftest.py)。排查环境问题时,先分清"哪个层级的谁"提供了某个行为。

禁用插件的几种姿势:

bash
pytest -p no:cacheprovider        # 本次运行禁用内置缓存插件
pytest --trace-config             # 先查名字
pytest --disable-plugin-autoload  # 8.4+ 关闭全部自动加载
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTEST_PLUGINS=cov,xdist pytest   # 只加载白名单

要把某项禁用固化为项目约定,写进配置文件的 addopts 即可:

toml
# content of pyproject.toml
[tool.pytest.ini_options]
addopts = ["-p", "no:cacheprovider"]   # 项目内永久关闭 cache 插件

显式控制时避免重复注册

官方文档特别提示:使用 --disable-plugin-autoload 时,不要再通过多种机制同时指定同一插件(如既 -p xdistPYTEST_PLUGINS=xdist),重复注册会导致错误。

12.3 常用插件盘点

结合本课程后续章节,重点认识这些官方文档点名的高人气插件:

插件一句话定位本课程出现位置
pytest-cov覆盖率统计,兼容分布式运行第 13 章
pytest-xdist多 CPU/多主机并行执行第 14 章
pytest-timeout按标记或全局设置给测试加超时,防止卡死本章实战
pytest-rerunfailures失败自动重跑,治理 flaky 测试第 20 章
pytest-instafail失败即时报告,不等整轮跑完
pytest-django / pytest-bdd框架级/BDD 级集成

pytest-timeout 为例感受"零配置可用 + 配置可调"的插件体验:

python
# content of test_timeout.py
import time
import pytest


@pytest.mark.timeout(0.5)          # 单测级别:该测试超过 0.5 秒判失败
def test_slow_query():
    time.sleep(5)
    assert True
toml
# content of pyproject.toml —— 全局默认 10 秒,防止 CI 卡死
[tool.pytest.ini_options]
addopts = ["--timeout=10"]

完整插件目录可在 PyPI 以 pytest- 前缀检索;官方还维护了按 pytest/Python 版本测试状态的插件列表页面。

另一个高频场景是给慢测试自动打标记——用几行 hook 代码就能实现"类插件"的行为,这也是第 16 章自定义插件的预告:

python
# content of conftest.py —— 收集期为 integration 模块的测试自动加 slow 标记
import pytest


def pytest_collection_modifyitems(config, items):
    for item in items:
        if "slow" in item.keywords:      # 已显式标记的不重复处理
            continue
        if getattr(item.function, "__module__", "").endswith("integration"):
            item.add_marker(pytest.mark.slow)

12.4 选型建议:少即是多

初学者常见误区是一次装十几个插件。每个插件都会改变收集、执行或报告行为——插件越多,"我的 pytest 和你行为不一样"式的环境问题越难排查。建议:

  1. 零插件起步,遇到真实痛点再引入对应插件;
  2. required_plugins(第 11 章)锁定团队必需插件及最低版本;
  3. CI 与本地保持同一份 pyproject.toml 配置,差异只通过 PYTEST_ADDOPTS 注入。

12.5 本章小结

  • entry-point(pytest11)让插件"装了就生效";pytest_plugins 变量与 -p 选项提供显式加载通道;
  • --trace-config 查看激活的插件,-p no:NAME 单次禁用,--disable-plugin-autoload 关闭自动加载(8.4+);
  • pytest-cov/xdist/timeout/rerunfailures 是生产项目最常用的四件套;
  • 插件宁缺毋滥,用 required_plugins 固化依赖。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. pip install pytest-timeout 后无需任何配置即被 pytest 加载,靠的机制是?

2. 想看当前运行激活了哪些插件及其来源,应执行?

3. 本次运行临时禁用内置 cache 插件,正确的命令是?

4. 关于 pytest_plugins 变量,说法正确的是?

🛠️ 动手实践

  1. 安装 pytest-timeout,给一个 time.sleep(10) 的测试打上 @pytest.mark.timeout(1) 标记,观察失败输出,再用 -p no:timeout 对比禁用后的行为。
  2. 运行 pytest --trace-config 记录你的环境中激活的全部插件;尝试用 -p no:cacheprovider 运行两次并检查 .pytest_cache 是否不再更新。
  3. 在根 conftest.py 里用 pytest_plugins 声明一个你自己写的最小插件模块(哪怕只有一个空 hook),验证它能被加载。

下一章把最重量级的插件 pytest-cov 讲透:覆盖率报告怎么读、分支覆盖率是什么、如何在 CI 上设质量门禁:第 13 章