← 返回题目列表

pytest 的 conftest.py 和插件机制是怎么回事?常用插件有哪些?

中等 第 18 / 27 题 更新于 2026/08/03
pytestconftest插件钩子测试基础设施

简化版

conftest.py 是 pytest 的「本地插件」——它不需要 import,pytest 会自动发现并加载,而且作用范围是它所在的目录及其所有子目录(所以可以在根目录放全局的 fixture、在 tests/api/ 放该模块专用的)。它能定义三样东西:fixture钩子函数(hook)、以及自定义标记和命令行选项pytest 的整个架构就是插件架构——连 parametrizefixtureassert 重写这些核心功能都是以内置插件形式实现的,所有扩展点都通过钩子暴露出来。最常用的五个钩子pytest_addoption(加命令行选项,比如 --env=staging)、pytest_configure(注册自定义标记、初始化全局资源)、pytest_collection_modifyitems收集完成后批量改用例——按标记跳过、自动加标记、重排顺序)、pytest_runtest_makereport(拿到每个测试的结果,用来在失败时保存现场)、pytest_generate_tests(动态参数化)。必装的插件按价值排:pytest-cov(覆盖率)、pytest-xdist(并行,-n auto)、pytest-randomly(打乱顺序暴露依赖)、pytest-timeout(防卡死)、pytest-mockmocker fixture,自动还原),再往后是 pytest-asynciopytest-djangorespxfreezegun两个容易踩的点conftest.py 里的 fixture 不能被 import(它靠名字注入,所以 IDE 常报「未定义」);多层 conftest 的 fixture 同名时,离测试最近的那个生效(这是特性,可以用来局部覆盖)。核心记忆:conftest 自动加载、作用域是所在目录及子目录pytest 是插件架构、扩展靠钩子五个常用钩子必装 cov/xdist/randomly/timeout/mock

详细版

conftest.py 能做的四件事

用途说明
定义 fixture自动可用,不需要 import
实现钩子修改 pytest 的行为
注册标记/选项pytest_configurepytest_addoption
共享工具函数但需要显式 import(不推荐)
# ① ★conftest.py 的层级结构★
# tests/
# ├── conftest.py           ★← 全局 fixture(所有测试可用)★
# ├── unit/
# │   └── conftest.py       ★← 只对 unit/ 下的测试生效★
# └── api/
#     ├── conftest.py       ★← 只对 api/ 下的测试生效★
#     └── test_users.py
# ★★同名 fixture:离测试最近的覆盖远的★★

# ② ★★pytest_addoption:自定义命令行选项★★
# tests/conftest.py
def pytest_addoption(parser):
    parser.addoption("--env", action="store", default="local",
                     choices=["local", "staging", "prod"], help="测试环境")
    parser.addoption("--runslow", action="store_true", help="运行慢测试")
    parser.addoption("--db-url", action="store", default=None)

@pytest.fixture(scope="session")
def env(request):
    return request.config.getoption("--env")      # ★★读取选项★★
# ★用法:pytest --env=staging★

# ③ ★★pytest_configure:注册标记(消除警告)★★
def pytest_configure(config):
    config.addinivalue_line("markers", "slow: 慢测试,默认跳过")
    config.addinivalue_line("markers", "integration: 需要外部依赖")
    config.addinivalue_line("markers", "flaky: 已知不稳定")
# ★不注册的话用 -W error 时会报 PytestUnknownMarkWarning★
# ★也可以写在 pytest.ini 的 markers = 下★

# ④ ★★pytest_collection_modifyitems:批量改用例★★
def pytest_collection_modifyitems(config, items):
    # ★场景一:没加 --runslow 就跳过慢测试★
    if not config.getoption("--runslow"):
        skip_slow = pytest.mark.skip(reason="需要 --runslow")
        for item in items:
            if "slow" in item.keywords:
                ★item.add_marker(skip_slow)★

    # ★场景二:按路径自动加标记(不用每个文件手写)★
    for item in items:
        if "/integration/" in str(item.fspath):
            ★item.add_marker(pytest.mark.integration)★
        if "/unit/" in str(item.fspath):
            item.add_marker(pytest.mark.unit)

    # ★场景三:把慢测试排到最后(快速反馈)★
    ★items.sort(key=lambda i: "slow" in i.keywords)★

# ⑤ ★★pytest_runtest_makereport:失败时保存现场★★
@pytest.hookimpl(★hookwrapper=True★, tryfirst=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    rep = outcome.get_result()
    ★item.stash[phase_key] = rep★                  # 存起来给 fixture 用
    if rep.when == "call" and rep.failed:
        # ★失败时截图/存日志/dump 数据库状态★
        logger.error("测试失败: %s", item.nodeid)

# ★fixture 里判断本次测试是否失败★
@pytest.fixture
def browser(request):
    driver = start_browser()
    yield driver
if request.node.rep_call.failed:★             # 需要上面的钩子配合
        driver.save_screenshot(f"fail_{request.node.name}.png")
    driver.quit()

# ⑥ ★★pytest_generate_tests:动态参数化★★
def pytest_generate_tests(metafunc):
    if "backend" in metafunc.fixturenames:
        backends = metafunc.config.getoption("--backends").split(",")
        ★metafunc.parametrize("backend", backends)★
# ★pytest --backends=sqlite,postgres★

# ⑦ ★常用插件★
# pytest-cov      --cov=myapp --cov-report=term-missing --cov-fail-under=80
# pytest-xdist    -n auto --dist=loadfile
# pytest-randomly (自动生效,打乱顺序)
# pytest-timeout  --timeout=60
# pytest-mock     def test_x(★mocker★): mocker.patch(...)  ★自动还原★
# pytest-sugar    更好看的进度条
# pytest-clarity  更清晰的 diff

⚠️ 三个必须记住的点:① conftest.py 不需要(也不应该)被 import——pytest 在收集测试时会自动发现并加载它,里面定义的 fixture 通过「参数名匹配」注入到测试函数。这带来两个后果:一是 IDE 经常报「未定义的名称」(因为静态分析看不出这种注入关系,PyCharm 和 VS Code 的 pytest 插件能识别,纯 linter 不行);二是 conftest 里的普通函数不会自动可用——如果你想共享工具函数,要么做成 fixture,要么放到单独的模块里显式 import(放 conftest 里再 import 是反模式,因为 conftest 的路径不稳定)。② conftest.py 的作用域是「所在目录及其所有子目录」,而且可以多层嵌套——根目录的 conftest 对所有测试生效、tests/api/conftest.py 只对 api 目录生效。同名 fixture 时,离测试文件最近的那个覆盖上层的——这是特性而非冲突,常用来「全局定义一个默认实现、在特定目录换成专用实现」。③ pytest_collection_modifyitems 是最强大的钩子,因为它能在「收集完成、执行开始之前」拿到所有测试用例的列表并任意修改:批量加标记(按目录自动分类,不用每个文件手写)、按条件跳过(没传 --runslow 就跳过慢测试)、重新排序(快测试先跑,实现快速反馈)、甚至过滤掉一部分。很多「测试基础设施」的需求都靠它实现

完整版教学

一、conftest.py 的机制

★ ★pytest 怎么发现 conftest★:
  ① 从 ★rootdir★ 开始(由 pytest.ini / pyproject.toml 等确定)
  ② ★沿着测试文件的路径逐层向上收集 conftest.py★
  ③ ★从最外层到最内层依次加载★
  ★ 例:运行 tests/api/v1/test_users.py 时加载:
    ★conftest.py(项目根)★
    ★tests/conftest.py★
    ★tests/api/conftest.py★
    ★tests/api/v1/conftest.py★

★ ★★fixture 的解析优先级(就近覆盖)★★:
  # tests/conftest.py
  @pytest.fixture
  def client(): return TestClient(app)          # ★默认★
  # tests/api/conftest.py
  @pytest.fixture
  def client(): return AuthedTestClient(app)    # ★★api/ 下用这个★★
  ★ ★不是冲突,是有意的覆盖机制★
  ★ 也可以复用上层的:
    @pytest.fixture
    def client(★client★):                       # ★★同名参数拿到上层的★★
        client.headers["X-Test"] = "1"
        return client

★ ★conftest 里能放什么★:
  ✓ ★fixture★(最主要的用途)
  ✓ ★钩子函数★(pytest_ 开头的)
  ✓ ★pytest_plugins = ["myplugin"]★(★只能在 rootdir 的 conftest★)
  ✗ ★普通工具函数★(不会自动可用,要 import——★但 conftest 不该被 import★)
    → ★放 tests/helpers.py 里★
  ✗ ★测试用例★(不会被收集)

★ ★★为什么 conftest 不该被 import★★:
  ✗ from tests.conftest import make_user
  ★ 原因:
    ① ★conftest 的加载由 pytest 控制,import 会重复执行模块级代码★
    ② ★rootdir 变化时 import 路径会失效★
    ③ ★多个 conftest 同名时 import 哪个不确定★
  ✓ ★共享工具函数放 tests/helpers.py 或 tests/factories.py★
  ✓ ★或者做成 fixture★:
    @pytest.fixture
    def make_user():
        def _make(**kw): return User(**{**DEFAULTS, **kw})
        return _make                             # ★★返回工厂函数★★
    def test_x(make_user):
        u = make_user(name="alice")

★ ★rootdir 和配置文件★:
  ★rootdir 的确定顺序★:
    ① 命令行参数的公共父目录往上找
    ② ★找 pytest.ini / pyproject.toml([tool.pytest.ini_options]) /
       tox.ini / setup.cfg★
  ★ ✗ ★rootdir 不对会导致 conftest 加载不全、相对路径错乱★
  ✓ ★项目根放一个 pyproject.toml 明确 rootdir★
  ✓ pytest 输出的第一行会打印 rootdir,★出问题先看它★

★ ★testpaths 与 pythonpath★:
  [tool.pytest.ini_options]
  ★testpaths = ["tests"]★                        # ★不传参数时默认测哪里★
  ★pythonpath = ["src"]★                         # ★★src layout 必备(7.0+)★★
  addopts = "-ra --strict-markers"
  ★ --strict-markers:★用了未注册的标记直接报错★(防拼写错误)

pytest 从 rootdir 开始沿路径逐层向上收集 conftest.py,从外到内依次加载——所以运行 tests/api/v1/test_x.py 时会加载四层 conftest。同名 fixture 就近覆盖是特性而非冲突,而且可以用同名参数拿到上层的 fixture 再加工def client(client):)。conftest 不该被 import——因为它的加载由 pytest 控制、rootdir 变化时路径会失效、多个同名时不确定 import 哪个;共享工具函数应该放 tests/helpers.py,或者做成「返回工厂函数」的 fixture。配置上有两个关键项:pythonpath = ["src"] 是 src layout 的必备(pytest 7.0+),--strict-markers 让未注册的标记直接报错(防止 @pytest.mark.slwo 这种拼写错误静默失效)。

二、钩子机制

★ ★pytest 的插件架构(pluggy)★:
  ★ pytest 自身的核心功能都是内置插件:
    - fixtures、parametrize、assertion rewriting
    - capture(捕获输出)、tmpdir、monkeypatch
    - junitxml、cacheprovider
  ★ ★所有扩展点都是"钩子"(hook)★
  ★ 三种实现钩子的位置:
    ① ★conftest.py(本地插件)★
    ② ★安装的第三方插件★
    ③ ★setuptools entry point(自己发布插件)★

★ ★★测试的生命周期与对应钩子★★:
  ┌──────────────────────────────────────────────────────┐
  │ ★启动★                                                │
  │   pytest_addoption(parser)        ★加命令行选项★       │
  │   pytest_configure(config)        ★注册标记、初始化★   │
  │   pytest_sessionstart(session)                        │
  │ ★收集★                                                │
  │   pytest_collection(session)                          │
  │   pytest_generate_tests(metafunc) ★动态参数化★         │
  │   ★pytest_collection_modifyitems(config, items)★       │
  │                                   ★★批量改用例★★        │
  │ ★执行(每个测试)★                                     │
  │   pytest_runtest_setup(item)      ★setup 前★           │
  │   pytest_runtest_call(item)       ★执行测试体★         │
  │   pytest_runtest_teardown(item)                       │
  │   ★pytest_runtest_makereport(item, call)★  ★生成报告★   │
  │ ★结束★                                                │
  │   pytest_sessionfinish(session, exitstatus)           │
  │   ★pytest_terminal_summary(terminalreporter)★ ★自定义汇总★│
  └──────────────────────────────────────────────────────┘

★ ★hookwrapper:包裹型钩子★:
  @pytest.hookimpl(★hookwrapper=True★)
  def pytest_runtest_makereport(item, call):
      # ★yield 之前:其他实现执行前★
      ★outcome = yield★                          # ★★让其他实现先跑★★
      rep = outcome.get_result()                # ★拿到结果★
      # ★yield 之后:可以修改结果★
      if rep.failed and "known_issue" in item.keywords:
          rep.outcome = "skipped"               # ★★改判★★
  ★ ★用途:需要"前后都做事"或"修改其他插件的结果"★

★ ★hookimpl 的顺序控制★:
  @pytest.hookimpl(★tryfirst=True★)              # 尽量先执行
  @pytest.hookimpl(★trylast=True★)               # 尽量后执行
  ★ ★多个插件实现同一个钩子时用来排序★

★ ★firstresult 钩子★:
  ★ 有些钩子是"第一个返回非 None 的结果生效"★
  # 例:自定义 assert 的失败信息
  def pytest_assertrepr_compare(config, op, left, right):
      if isinstance(left, Money) and isinstance(right, Money) and op == "==":
          return [f"金额不相等:",
                  f"  实际: {left}", f"  期望: {right}",
                  f"  差额: {left - right}"]
  ★ ✓ ★让领域对象的断言失败信息可读★

★ ★常用钩子速查★:
  ┌────────────────────────────────┬──────────────────────┐
  │ pytest_addoption               │ ★命令行选项★          │
  │ pytest_configure               │ ★注册标记、初始化★    │
  │ ★pytest_collection_modifyitems★│ ★★批量改用例(最有用)★★│
  │ pytest_generate_tests          │ ★动态参数化★          │
  │ pytest_runtest_makereport      │ ★拿测试结果★          │
  │ pytest_runtest_setup           │ ★每个测试前的检查★    │
  │ pytest_assertrepr_compare      │ ★自定义断言信息★      │
  │ pytest_terminal_summary        │ ★自定义结尾汇总★      │
  │ pytest_sessionfinish           │ 全部结束后            │
  └────────────────────────────────┴──────────────────────┘

★ ★写自己的插件(发布复用)★:
  # pyproject.toml
  [project.entry-points.pytest11]
  ★myplugin = "mypackage.pytest_plugin"★
  ★ → 安装后自动生效,不用写 conftest
  ★ ✓ 适合:★团队内多个项目共享的测试基础设施★

pytest 的整个架构建立在 pluggy 插件系统上——连 fixture、parametrize、assertion rewriting 这些核心功能都是内置插件。钩子按测试生命周期分成启动、收集、执行、结束四个阶段hookwrapper=True 是包裹型钩子——yield 之前是其他实现执行前、yield 之后能拿到并修改结果(比如把已知问题的失败改判成 skip)。pytest_assertrepr_compare 很实用但少有人知——它能让领域对象的断言失败信息变得可读(比如「金额不相等,差额 X 元」而不是一堆对象 repr)。如果测试基础设施要在多个项目间共享,可以做成真正的插件用 entry point pytest11 发布,安装后自动生效。

三、必装插件详解

★ ★① pytest-cov(覆盖率)★
  pytest ★--cov=myapp --cov-report=term-missing --cov-report=html★
  # pyproject.toml
  [tool.coverage.run]
  ★source = ["src/myapp"]★
  ★branch = true★                                # ★★分支覆盖率(比行覆盖有意义)★★
  omit = ["*/migrations/*", "*/tests/*"]
  [tool.coverage.report]
  ★fail_under = 80★
  ★exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:",
                   "raise NotImplementedError"]★
  ★ ★注意:和 xdist 一起用时要 --cov-append 或用 coverage combine★

★ ★★② pytest-xdist(并行)★★
  pytest ★-n auto★                                # 按 CPU 核数
  pytest -n 4 ★--dist=loadfile★                   # ★同文件的测试在同一 worker★
  pytest -n 4 --dist=loadscope                   # ★同 class/module 在同 worker★
  ★ ✓ ★大套件提速最直接的手段★
  ★ ✗ 坑:
    - ★测试间不能共享状态★(各 worker 是独立进程)
    - ★session 级 fixture 每个 worker 都会执行一遍★
    - ★数据库/端口/临时文件要按 worker 隔离★
    - ★输出顺序乱★(用 -q 或看报告文件)
  ✓ ★worker_id fixture 做隔离★:
    @pytest.fixture(scope="session")
    def db_name(★worker_id★):
        return "test" if worker_id == "master" else f"test_{worker_id}"

★ ★③ pytest-randomly(打乱顺序)★
  ★ ✓ ★每次运行打乱测试顺序 → 暴露顺序依赖★
  ★ ✓ ★自动为 random/numpy 设种子并打印★
  ★ ✓ ★失败时用 --randomly-seed=X 完全复现★
  pytest ★-p no:randomly★                         # 临时关闭
  ★ ★新项目建议一开始就装★(顺序依赖是逐渐累积的)

★ ★④ pytest-timeout(防卡死)★
  pytest ★--timeout=60★
  ★@pytest.mark.timeout(300)★                     # 单个测试放宽
  # pyproject.toml
  timeout = 60
  ★timeout_method = "thread"★                     # 或 "signal"
  ★ ✓ ★CI 上必装★——避免一个卡死的测试让整个 job 挂 6 小时

★ ★⑤ pytest-mock(mocker fixture)★
  def test_x(★mocker★):
      m = ★mocker.patch("app.service.send_email")★  # ★★自动还原★★
      mocker.patch.object(obj, "method")
      spy = ★mocker.spy(module, "func")★            # ★保留真实行为 + 记录调用★
      stub = ★mocker.stub(name="callback")★
  ★ ✓ ★比 unittest.mock.patch 少写装饰器嵌套★
  ★ ✓ ★测试结束自动 undo,不会泄漏★

★ ★其他常用★:
  ┌──────────────────────┬────────────────────────────┐
  │ ★pytest-asyncio★      │ 异步测试(asyncio_mode=auto)│
  │ ★pytest-django★       │ Django 集成(db fixture)   │
  │ ★pytest-flask★        │ Flask 集成                  │
  │ ★pytest-httpx/respx★  │ ★拦截 HTTP★                │
  │ ★pytest-socket★       │ ★★禁用真实网络★★           │
  │ ★pytest-benchmark★    │ 性能基准                    │
  │ ★pytest-repeat★       │ --count=N 反复跑            │
  │ ★pytest-rerunfailures★│ 重试(★慎用★)              │
  │ pytest-sugar         │ 好看的进度条                 │
  │ ★pytest-clarity★      │ ★更清晰的 assert diff★     │
  │ ★pytest-env★          │ 设置环境变量                │
  │ ★pytest-freezegun★    │ 冻结时间                    │
  └──────────────────────┴────────────────────────────┘

★ ★插件之间的冲突★:
  ★ ✗ pytest-xdist + pytest-cov:★需要 --cov-append 或配置★
  ★ ✗ pytest-randomly + 依赖顺序的插件
  ★ ✗ 多个 asyncio 插件(pytest-asyncio vs anyio)★别同时用★
  ✓ ★pytest --trace-config 看加载了哪些插件★
  ✓ ★pytest -p no:插件名 临时禁用★

五个必装插件里:pytest-cov 要开 branch = true(分支覆盖率比行覆盖有意义得多),和 xdist 一起用时要处理 --cov-appendpytest-xdist 是大套件提速最直接的手段,但要注意各 worker 是独立进程——session 级 fixture 会执行多遍、数据库和端口要按 worker_id 隔离。pytest-timeout 在 CI 上必装——避免一个卡死的测试让整个 job 跑 6 小时。pytest-mockmocker fixture 比 unittest.mock.patch 好用(少写装饰器嵌套、自动还原、还有 mocker.spy 保留真实行为同时记录调用)。排查插件问题用 pytest --trace-config 看加载了哪些、-p no:插件名 临时禁用。

四、标记与选择性执行

★ ★注册标记(避免拼写错误)★:
  # pyproject.toml
  [tool.pytest.ini_options]
  markers = [
      "slow: 慢测试(默认跳过,用 --runslow 启用)",
      "integration: 需要数据库等外部依赖",
      "e2e: 端到端测试",
      "flaky: 已知不稳定(必须带 issue 号)",
  ]
  ★addopts = "--strict-markers"★                 # ★★未注册的标记直接报错★★
  ★ ✗ 不加 --strict-markers 时,@pytest.mark.slwo(拼错)会★静默失效★

★ ★★-m 表达式(组合筛选)★★:
  pytest ★-m slow★
  pytest ★-m "not slow"★
  pytest ★-m "integration and not slow"★
  pytest ★-m "unit or smoke"★
  ★ ✓ ★CI 里分阶段跑的基础★

★ ★-k 按名字筛(模糊)★:
  pytest ★-k "user"★                              # 名字含 user
  pytest -k "user and not admin"
  pytest ★-k "test_login or test_logout"★
  ★ ★-k 匹配的是测试名和参数化 id★

★ ★★按标记自动跳过(最常用的模式)★★:
  # conftest.py
  def pytest_collection_modifyitems(config, items):
      if config.getoption("--runslow"):
          return
      skip = pytest.mark.skip(reason="慢测试,加 --runslow 运行")
      for item in items:
          if "slow" in item.keywords:
              item.add_marker(skip)
  ★ ✓ ★默认快速反馈,需要时再跑全量★

★ ★条件跳过★:
  ★@pytest.mark.skipif(sys.version_info < (3, 11), reason="需要 3.11+")★
  ★@pytest.mark.skipif(not shutil.which("docker"), reason="需要 docker")★
  ★@pytest.mark.xfail(reason="已知 bug #123", strict=True)★
  ★ ★strict=True:修复后变成 XPASS 会算失败,提醒你删标记★
  # 运行时跳过
  def test_x():
      if not has_gpu(): ★pytest.skip("需要 GPU")★
  # ★整个模块跳过★
  ★pytestmark = pytest.mark.skipif(...)★          # 模块级变量

★ ★★CI 分阶段的典型配置★★:
  # ★阶段一:快速反馈(每次 push,2 分钟)★
  pytest -m "not slow and not integration and not e2e" -n auto
  # ★阶段二:集成测试(PR 合并前,10 分钟)★
  pytest -m "integration" --maxfail=5
  # ★阶段三:全量 + 慢测试(nightly)★
  pytest --runslow -n auto
  # ★阶段四:flaky 隔离 job(允许失败)★
  pytest -m flaky || true

★ ★按目录自动打标记(省去手写)★:
  def pytest_collection_modifyitems(config, items):
      for item in items:
          p = str(item.fspath)
          if "/tests/unit/" in p: item.add_marker(pytest.mark.unit)
          elif "/tests/integration/" in p: item.add_marker(pytest.mark.integration)
          elif "/tests/e2e/" in p: item.add_marker(pytest.mark.e2e)
  ★ ✓ ★目录结构即分类,不用每个文件加装饰器★

★ ★缓存与增量运行★:
  pytest ★--lf★         # ★只跑上次失败的★
  pytest ★--ff★         # ★先跑上次失败的,再跑其他★
  pytest ★--nf★         # 先跑新增的文件
  pytest ★--sw★         # ★stepwise:失败就停,下次从那里继续★
  pytest ★--cache-clear★
  ★ ★本地开发时 --lf 和 --sw 能大幅提升迭代速度★

标记要在配置里注册并开 --strict-markers——否则 @pytest.mark.slwo(拼错)会静默失效,你以为标记了慢测试其实没有。-m 支持表达式组合"integration and not slow"),这是 CI 分阶段执行的基础。「按标记自动跳过」是最常用的模式——默认跳过慢测试保证快速反馈,加 --runslow 才跑。xfail 建议加 strict=True——bug 修复后会变成 XPASS 算失败,提醒你删掉标记。按目录自动打标记能省去在每个文件手写装饰器。本地开发时 --lf(只跑上次失败的)和 --sw(stepwise)能大幅提升迭代速度

五、实用的 conftest 模式

★ ★模式一:工厂 fixture(★比直接返回对象灵活★)★
  @pytest.fixture
  def make_user(db):
      created = []
      def _make(**kwargs):
          u = User(**{"name": "test", "email": "t@x.com", **kwargs})
          db.add(u); db.flush()
          created.append(u)
          return u
      yield _make
      for u in created: db.delete(u)             # ★★统一清理★★
  # 用法
  def test_x(make_user):
      alice = make_user(name="alice")
      bob = make_user(name="bob", is_admin=True)

★ ★模式二:autouse 做全局清理★
  @pytest.fixture(★autouse=True★)
  def _reset_state():
      yield
      cache.clear()
      app.dependency_overrides.clear()
      SomeSingleton._instance = None
  ★ ✓ ★不用每个测试记得清理★
  ★ ✗ ★autouse 太多会让测试变慢且难以理解★ → 只用于必要的清理

★ ★模式三:根据标记做 setup★
  @pytest.fixture(autouse=True)
  def _setup_by_marker(request):
      if request.node.get_closest_marker("integration"):
          ★start_docker_services()★
          yield
          stop_docker_services()
      else:
          yield

★ ★模式四:session 级的重资源 + function 级的隔离★
  @pytest.fixture(scope="session")
  def engine():                                  # ★★整个 session 建一次★★
      e = create_engine(TEST_DB_URL)
      Base.metadata.create_all(e)
      yield e
      Base.metadata.drop_all(e); e.dispose()

  @pytest.fixture                                # ★function 级★
  def db(engine):
      conn = engine.connect(); trans = conn.begin()
      s = Session(bind=conn)
      yield s
      s.close(); ★trans.rollback()★; conn.close()  # ★★每个测试回滚★★
  ★ ★这是"慢资源共享 + 数据隔离"的标准组合★

★ ★模式五:捕获日志和输出★
  def test_logs(★caplog★):                        # ★pytest 内置★
      with caplog.at_level(logging.WARNING):
          do_something()
      ★assert "库存不足" in caplog.text★
      ★assert caplog.records[0].levelname == "WARNING"★

  def test_output(★capsys★):
      print("hello")
      ★assert capsys.readouterr().out == "hello\n"★

★ ★模式六:临时文件和目录★
  def test_file(★tmp_path★):                      # ★pathlib.Path,自动隔离★
      f = tmp_path / "test.txt"
      f.write_text("content")
      assert process_file(f) == "CONTENT"
  ★ ★tmp_path 每个测试独立,pytest 自动清理(保留最近 3 次)★
  ★ tmp_path_factory:session 级的临时目录

★ ★模式七:跳过整个目录(缺少依赖时)★
  # tests/integration/conftest.py
  ★pytest.importorskip("docker")★                 # ★没装就跳过整个目录★
  ★collect_ignore = ["test_gpu.py"] if not has_gpu() else []★

★ ★模式八:终端汇总自定义★
  def pytest_terminal_summary(terminalreporter, exitstatus, config):
      slow = [r for r in terminalreporter.stats.get("passed", [])
              if r.duration > 1.0]
      if slow:
          terminalreporter.write_sep("=", "★慢测试 TOP 10★")
          for r in sorted(slow, key=lambda x: -x.duration)[:10]:
              terminalreporter.write_line(f"{r.duration:.2f}s {r.nodeid}")
  ★ ✓ ★每次跑完自动提醒哪些测试变慢了★

八个实用模式里几个高价值的:工厂 fixturemake_user(name="alice") 比固定对象灵活,还能统一清理)、session 级重资源 + function 级隔离(建库一次、每个测试事务回滚,这是标准组合)、caplogcapsys(pytest 内置,测日志和输出)、tmp_path(每个测试独立的临时目录,自动清理)、pytest.importorskip(缺少依赖时跳过整个目录)。最后那个 pytest_terminal_summary 自定义汇总很实用——每次跑完自动列出最慢的 10 个测试,能持续发现性能退化。

六、实践清单

★ ★推荐的 pyproject.toml 配置★:
  [tool.pytest.ini_options]
  ★testpaths = ["tests"]★
  ★pythonpath = ["src"]★                         # ★src layout 必备★
  addopts = [
      "-ra",                                     # ★显示所有非通过的原因★
      ★"--strict-markers"★,                       # ★未注册标记报错★
      ★"--strict-config"★,                        # 配置项拼错报错
      "--tb=short",
      ★"--durations=10"★,                         # ★★显示最慢的 10 个★★
      "--cov=src", "--cov-report=term-missing",
  ]
  markers = [
      "slow: 慢测试",
      "integration: 需要外部依赖",
      "e2e: 端到端",
      "flaky: 已知不稳定(必须带 issue 号)",
  ]
  ★filterwarnings = ["error", "ignore::DeprecationWarning:third_party.*"]★
  ★timeout = 60★

★ 检查清单:
  【conftest】
  □ ★conftest 不被 import(工具函数放 helpers.py)★
  □ ★分层放置(全局的放根,专用的放子目录)★
  □ ★autouse fixture 只用于必要的清理★
  【标记】
  □ ★所有自定义标记都注册了★
  □ ★开了 --strict-markers★
  □ ★xfail 用 strict=True★
  【插件】
  □ ★装了 cov / xdist / randomly / timeout / mock★
  □ ★CI 上配了 --timeout★
  □ ★xdist 下资源按 worker_id 隔离★
  【CI】
  □ ★分阶段执行(快速反馈 / 集成 / nightly)★
  □ ★--durations 监控慢测试★
  □ ★junitxml 归档用于统计★

★ ★排查 conftest/插件问题★:
  pytest ★--collect-only★                         # 看收集到哪些用例
  pytest ★--fixtures★                             # ★★列出所有可用 fixture 及来源★★
  pytest ★--fixtures-per-test★                    # 每个测试用了哪些 fixture
  pytest ★--trace-config★                         # ★加载了哪些插件和 conftest★
  pytest ★--markers★                              # 列出所有标记
  pytest ★--setup-show★                           # ★★显示 fixture 的执行顺序★★
  pytest ★-p no:randomly★                         # 禁用某个插件

★ ★常见问题★:
  ┌────────────────────────────────────┬──────────────────┐
  │ fixture not found                   │ ★conftest 位置不对★│
  │ 标记不生效                          │ ★拼写错(开 strict)★│
  │ import 报 ModuleNotFoundError       │ ★pythonpath 没配★  │
  │ 加了 -n 后 session fixture 跑多次    │ ★★各 worker 独立★★ │
  │ 覆盖率在 xdist 下不对                │ ★--cov-append★     │
  │ rootdir 不对导致配置没生效           │ ★看输出第一行★     │
  └────────────────────────────────────┴──────────────────┘

★ 一句话总结:
  ★"conftest.py 是自动加载的本地插件,作用范围是所在目录及子目录,
    同名 fixture 就近覆盖;pytest 本身是插件架构,扩展靠钩子——
    最有用的是 pytest_collection_modifyitems(批量改用例)、
    pytest_addoption(命令行选项)、pytest_configure(注册标记);
    必装 cov/xdist/randomly/timeout/mock,
    标记要注册并开 --strict-markers。"★

推荐配置里几个值得加的:--strict-markers(未注册标记报错)、--strict-config(配置项拼错报错)、--durations=10(显示最慢的 10 个测试)、filterwarnings = ["error", ...](把警告当错误,及早发现弃用)。排查问题的命令里最有用的是 pytest --fixtures(列出所有可用 fixture 及其来源,解决「fixture not found」)和 pytest --setup-show(显示 fixture 的实际执行顺序)。

记忆钩子:「★conftest.py 是 pytest 的『本地插件』——不需要 import,pytest 自动发现并加载★,★作用范围是它所在的目录及所有子目录★,pytest 会★从 rootdir 沿测试文件路径逐层向上收集、从外到内依次加载★。★同名 fixture 时离测试最近的覆盖上层的——这是特性不是冲突★,还能★用同名参数拿到上层的再加工(def client(client))★。★conftest 不该被 import★(加载由 pytest 控制、rootdir 变化路径会失效、多个同名时不确定),★共享工具函数放 tests/helpers.py 或做成『返回工厂函数』的 fixture★。★pytest 本身就是插件架构(pluggy),连 fixture/parametrize/assert 重写都是内置插件★,扩展点全是钩子。★五个最常用的钩子★:★pytest_addoption(加命令行选项如 —env)★、★pytest_configure(注册标记、初始化)★、★pytest_collection_modifyitems(最强大——收集完成后拿到全部用例列表任意修改:按标记跳过、按目录自动打标记、重排顺序)★、★pytest_runtest_makereport(拿每个测试的结果,失败时截图存日志)★、★pytest_generate_tests(动态参数化)★;★hookwrapper=True 是包裹型钩子,yield 前后都能做事还能改结果★。★必装五插件★:★pytest-cov(要开 branch=true,和 xdist 一起用要 —cov-append)★、★pytest-xdist(-n auto 提速最直接,但各 worker 是独立进程所以 session fixture 会跑多遍、数据库端口要按 worker_id 隔离)★、★pytest-randomly(打乱顺序暴露依赖,新项目一开始就装)★、★pytest-timeout(CI 必装,避免卡死的测试跑 6 小时)★、★pytest-mock(mocker fixture 自动还原,还有 mocker.spy 保留真实行为同时记录调用)★。★标记必须注册并开 —strict-markers★——否则 @pytest.mark.slwo 拼错会★静默失效★;★xfail 要加 strict=True★(修复后变 XPASS 算失败,提醒你删标记)。★CI 靠 -m 表达式分阶段★:快速反馈跑 ‘not slow and not integration’、集成测试单独跑、全量放 nightly、★flaky 隔离到允许失败的 job★。实用配置:★pythonpath=[‘src’] 是 src layout 必备★、★—durations=10 显示最慢的测试★、★filterwarnings=[‘error’] 把警告当错误★。排查用 ★pytest —fixtures(列出所有可用 fixture 及来源)★、★—setup-show(显示 fixture 执行顺序)★、★—trace-config(看加载了哪些插件)★。」

七、常见误区与追问

  • 误区:conftest.py 里的东西要 import 才能用。 恰恰相反——conftest.py 不需要也不应该被 import。pytest 在收集测试时会自动发现路径上的所有 conftest.py 并加载,里面定义的 fixture 通过参数名匹配注入到测试函数(def test_x(db) 会去找名为 db 的 fixture)。手动 from tests.conftest import make_user 有三个问题:① 会导致模块被重复执行(pytest 已经加载过一次);② rootdir 或目录结构变化时 import 路径会失效③ 多层 conftest 同名时不确定 import 到哪个。如果确实需要共享普通工具函数(不是 fixture),正确做法是放到 tests/helpers.pytests/factories.py 里显式 import;或者做成返回工厂函数的 fixturedef make_user(): return _make),这样既能享受自动注入又能带参数。顺带一提,IDE 报「未定义的名称」是正常现象——静态分析看不出 fixture 的注入关系。
  • 误区:标记(marker)写上就生效了,不用注册。 不注册的话,拼写错误会静默失效@pytest.mark.slwo(把 slow 拼错)不会有任何报错——pytest 允许任意名字的标记,你以为标记了慢测试,实际上 -m slow 根本筛不到它,CI 里就漏跑了。解法是pyproject.tomlmarkers 里注册所有自定义标记,并且开启 --strict-markers——之后任何未注册的标记都会直接报错而不是静默通过。顺带推荐 --strict-config(配置项拼错时报错,防止 addopts 里写了个不存在的选项却没人发现)。注册标记还有个附带好处:pytest --markers 能列出所有标记及其说明,成为团队的测试分类文档。另外 @pytest.mark.xfail 建议加 strict=True——这样当 bug 被修复、测试意外通过时会报 XPASS 失败,提醒你把标记删掉,而不是让一个「已经不该 xfail 的标记」永远留在代码里。
  • 误区:加了 pytest -n auto 并行,测试就应该正常跑。 各个 worker 是完全独立的进程,这带来三类问题。① session 级 fixture 会执行多遍——scope="session" 的含义是「每个 session 一次」,而 4 个 worker 就是 4 个 session,所以「建数据库表」这种操作会跑 4 次(可能互相冲突)。② 共享资源冲突——多个 worker 同时写同一个数据库、绑定同一个端口、读写同一个临时文件。解法是用 pytest-xdist 提供的 worker_id fixture 做隔离:每个 worker 用独立的数据库名(test_gw0test_gw1)、独立的 Redis db 号、端口用 0 让系统分配。③ 测试间不能共享内存状态——任何依赖「前一个测试留下的全局变量」的写法都会崩(这其实是好事,它暴露了本来就存在的顺序依赖)。另外覆盖率统计在 xdist 下需要额外配置(--cov-appendcoverage combine),否则数据会不完整。
  • 误区:pytest_collection_modifyitems 只是个高级功能,用不太上。 它其实是实现「测试基础设施」最常用的钩子,因为它能在「所有用例收集完成、但还没开始执行」的时刻拿到完整的用例列表并任意修改。四个高频用途:① 按目录自动打标记——不用在每个测试文件顶部写 pytestmark = pytest.mark.integration,直接根据文件路径批量添加,目录结构即分类② 条件跳过——「没传 --runslow 就给所有 slow 标记的用例加上 skip」,实现「默认快速反馈、需要时跑全量」;③ 重新排序——把慢测试排到最后,让开发者更快看到快测试的结果;④ 过滤——在特定环境下移除某些用例。它的签名是 pytest_collection_modifyitems(config, items)items 是一个可变列表,你对它的增删改排序都会生效。几乎所有「我希望测试框架能自动帮我做 X」的需求,第一反应都应该是看这个钩子能不能实现
  • 误区:conftest.py 越集中越好,全放在根目录。 分层放置才是设计意图。pytest 支持多层 conftest 正是为了「不同范围用不同的基础设施」:根目录的 conftest.py所有测试都需要的(配置、全局清理、通用工厂);tests/api/conftest.pyAPI 测试专用的(HTTP client、认证 header);tests/unit/conftest.py单元测试专用的(Fake 对象)。这样做有三个好处:① 加载开销小——单元测试不会因为集成测试的 fixture 而付出代价;② 意图清晰——看 conftest 的位置就知道这个 fixture 的适用范围;③ 可以局部覆盖——同名 fixture 时离测试最近的生效,比如全局定义一个连真实数据库的 db,在 tests/unit/conftest.py 里覆盖成内存版。反过来,把所有东西堆在根 conftest 会让它膨胀到几百行、fixture 之间的依赖关系混乱、而且每个测试都要加载全部内容
  • 追问:pytest --fixtures--setup-show 分别解决什么问题? 两个都是排查 fixture 问题的利器。pytest --fixtures 列出当前上下文下所有可用的 fixture 及其来源文件和文档字符串——遇到 fixture 'db' not found 时,第一件事就是跑它看看:是不是 conftest 放错了位置(不在测试文件的路径上)、是不是插件没装、是不是名字拼错了。加上具体的测试文件路径(pytest --fixtures tests/api/test_users.py)能看到该文件实际可见的 fixture 集合。pytest --setup-show 则显示每个测试执行时 fixture 的实际调用顺序和作用域SETUP S engine / SETUP F db (fixtures used: engine) / TEARDOWN F db),用来回答「为什么这个 session fixture 执行了两次」「fixture 的销毁顺序对不对」「这个 autouse fixture 到底有没有生效」这类问题。配套还有 --fixtures-per-test(每个测试用了哪些 fixture)和 --trace-config(加载了哪些插件和 conftest,排查插件冲突时用)。
  • 追问:什么时候该把 conftest 里的东西抽成真正的 pytest 插件? 判断标准是**「是否需要跨项目复用」conftest.py 的作用范围限于当前项目的目录树,如果团队里有多个项目需要同样的测试基础设施——比如统一的数据库测试隔离方案、公司内部服务的 mock 工具、统一的测试报告格式——那就值得抽成独立的包。做法是在 pyproject.toml 里声明 entry point:[project.entry-points.pytest11] 下写 myplugin = "mypackage.pytest_plugin",安装这个包之后 pytest 会自动加载它,不需要任何 conftest 配置**。插件模块里可以定义 fixture 和钩子,写法和 conftest 完全一样。好处是:版本化管理(各项目可以用不同版本)、可测试(插件本身可以有自己的测试,pytest 提供了 pytester fixture 专门用于测插件)、文档化。成本是多了一个包要维护和发布——所以单个项目内的东西留在 conftest 就好,只有确实要共享时才抽出去。
  • 追问:pytest-mockmocker 相比直接用 unittest.mock.patch 好在哪? 三点。① 自动清理——mocker.patch(...) 打的补丁会在测试结束时自动撤销,不需要用装饰器或 with 语句管理作用域,也就不会出现「忘记 stop 导致污染后续测试」的问题(这是顺序依赖类 flaky 的常见来源)。② 写法更平——用 unittest.mock 打多个补丁时要么堆叠装饰器(@patch 的参数顺序是从下往上,很容易搞错),要么嵌套多层 with;而 mocker 是普通的函数调用,m1 = mocker.patch(...)m2 = mocker.patch(...) 一行一个,顺序直观。③ 额外的便利方法——mocker.spy(module, "func")保留原函数的真实行为同时记录调用(这正是「Spy」的准确形态,用 unittest.mock 要自己写 side_effect);mocker.stub() 创建带名字的桩;mocker.patch.objectmocker.patch.dict 也都有。唯一要注意的是它是第三方插件(要装 pytest-mock),而且底层仍然是 unittest.mock,所以 patch 路径的规则完全一样——依然要 patch「使用处」而不是「定义处」。

八、加强记忆

conftest.py 是 pytest 的「本地插件」——不需要 import,pytest 会自动发现并加载作用范围是它所在的目录及所有子目录;pytest 会从 rootdir 沿着测试文件的路径逐层向上收集、从外到内依次加载同名 fixture 时离测试最近的覆盖上层的——这是特性不是冲突,还能用同名参数拿到上层的再加工def client(client):)。conftest 不该被 import(加载由 pytest 控制、rootdir 变化时路径会失效、多个同名时不确定加载哪个),共享工具函数应该放 tests/helpers.py 或做成「返回工厂函数」的 fixturepytest 本身就是插件架构(pluggy),连 fixture、parametrize、assert 重写都是内置插件,所有扩展点都是钩子。五个最常用的钩子pytest_addoption(加命令行选项如 --envpytest_configure(注册标记、初始化)pytest_collection_modifyitems(最强大——收集完成后拿到全部用例列表任意修改:按标记跳过、按目录自动打标记、重排顺序)pytest_runtest_makereport(拿到每个测试的结果,失败时截图存日志)pytest_generate_tests(动态参数化)hookwrapper=True 是包裹型钩子,yield 前后都能做事、还能修改其他实现的结果必装的五个插件pytest-cov(要开 branch = true,和 xdist 一起用要 --cov-append)、pytest-xdist-n auto 是提速最直接的手段,但各 worker 是独立进程,所以 session fixture 会跑多遍、数据库和端口要按 worker_id 隔离)、pytest-randomly(打乱顺序暴露依赖,新项目一开始就装)、pytest-timeout(CI 必装,避免卡死的测试跑 6 小时)、pytest-mockmocker fixture 自动还原,还有 mocker.spy 保留真实行为同时记录调用)。标记必须注册并开 --strict-markers——否则 @pytest.mark.slwo 这种拼写错误会静默失效xfail 要加 strict=True(修复后变 XPASS 算失败,提醒你删标记)。CI 靠 -m 表达式分阶段执行:快速反馈跑 "not slow and not integration"、集成测试单独跑、全量放 nightly、flaky 隔离到允许失败的 job。几个实用配置:pythonpath = ["src"] 是 src layout 的必备--durations=10 显示最慢的测试filterwarnings = ["error"] 把警告当错误。排查问题用 pytest --fixtures(列出所有可用 fixture 及来源)--setup-show(显示 fixture 的执行顺序)--trace-config(看加载了哪些插件)