Skip to content

第 7 章 · 07 fixture 进阶:作用域与 conftest

本章目标:掌握 scope 四种作用域的实例化时机与共享语义,理解 conftest.py 的层级覆盖机制与 autouse 自动注入,并学会在"共享省钱"和"状态隔离"之间做正确取舍。

7.1 scope:fixture 活多久

默认情况下,每个测试都会拿到新创建的 fixture 实例(scope="function")。当构造昂贵时(如真实 SMTP 连接、数据库引擎、Docker 容器),可以提升作用域让实例被复用:

python
# content of conftest.py
import smtplib

import pytest


@pytest.fixture(scope="module")
def smtp_connection():
    # 整个测试模块只创建一次,模块内所有测试共享同一连接
    return smtplib.SMTP("smtp.gmail.com", 587, timeout=5)
python
# content of test_module.py —— 两个测试收到的是同一个对象
def test_ehlo(smtp_connection):
    response, msg = smtp_connection.ehlo()
    assert response == 250


def test_noop(smtp_connection):
    resp = smtp_connection.noop()
    assert resp[0] == 250

官方定义的五种取值及销毁时机:

scope实例化时机teardown 执行时机
function(默认)每个测试前该测试结束后
class每个测试类第一个测试前类内最后一个测试结束后
module每个模块第一个测试前模块最后一个测试结束后
package包内第一个测试前包内最后一个测试结束后
session整场会话第一次被请求时全部测试结束后

高作用域 + 可变状态 = 灾难

scope 提升到 module/session 后,所有测试共享同一份可变状态。一个测试往共享列表里 append、改了共享字典,后面的测试就会莫名失败。原则:高作用域 fixture 应该提供"只读或不可变"的资源(连接、配置对象),需要修改状态的资源保持 function 级。

7.2 conftest.py 层级:就近覆盖

conftest.py 是分层生效的:目录树上的每个 conftest 只对其所在目录及子目录可见。当两层 conftest 定义了同名 fixture 时,更靠近测试的那层获胜——这实现了"默认值 + 局部定制":

text
tests/
├── conftest.py               # def base_url(): return "https://api.prod.example.com"
└── local/
    ├── conftest.py           # def base_url(): return "http://localhost:8000"
    └── test_local.py         # 用到 base_url 时得到 localhost 版本
python
# tests/conftest.py
import pytest


@pytest.fixture
def base_url() -> str:
    """全项目默认指向预发环境"""
    return "https://staging.example.com"
python
# tests/local/conftest.py
import pytest


@pytest.fixture
def base_url() -> str:
    """local 目录下的测试全部改打本地服务——直接同名覆盖"""
    return "http://localhost:8000"

这个机制同样适用于对第三方插件的 fixture 做定制,是 pytest 实现"约定优于配置"的关键设计。

7.3 autouse:不需要请求的 fixture

有些横切关注点(临时目录切换、屏蔽网络、注入公共补丁)每个测试都需要,逐个写参数太啰嗦。autouse=True 让 fixture 被所有测试自动请求:

python
# tests/conftest.py
import os

import pytest


@pytest.fixture(autouse=True)
def _isolated_cwd(tmp_path):
    """让所有测试自动运行在临时目录里,避免污染仓库"""
    old = os.getcwd()
    os.chdir(tmp_path)          # setup:进入临时目录
    yield tmp_path
    os.chdir(old)               # teardown:回到原目录


@pytest.fixture(autouse=True)
def _fail_on_slow(monkeypatch):
    # autouse 也可以只做副作用(不返回值)
    monkeypatch.setenv("APP_ENV", "testing")
python
# 任何测试文件——无需声明任何参数就享受上述环境
def test_writes_config():
    with open("config.yaml", "w") as f:
        f.write("debug: true")
    assert os.path.exists("config.yaml")   # 写进的是 tmp_path,不会弄脏仓库

autouse 同样受 scope 控制;它也可以正常返回值,只是"不请求也能用"。克制使用:autouse 本质是隐式全局行为,数量失控后没人能说清某个测试的完整执行环境。

7.4 作用域冲突:高不能依赖低

一条硬规则:高层作用域的 fixture 不能依赖低层作用域的 fixturesession 级 fixture 若依赖 function 级 fixture,pytest 会直接报 ScopeMismatch 错误——因为 session 实例只建一次,而它依赖的东西却要每测试重建一次,逻辑上无法成立。

常见踩坑场景与解法:

python
@pytest.fixture(scope="session")
def engine():
    return create_engine("sqlite:///:memory:")


@pytest.fixture(scope="session")
def tables(engine):        # session 依赖 session ✔
    ...

如果确实需要在 session 级初始化中用到 per-test 数据,正确方向是反转结构:把"贵而不变"的部分做成高作用域,per-test 差异放在低作用域 fixture 中去引用它。

7.5 取舍决策表

需求选择
构造快、含可变状态(列表/模型对象)function(默认)
昂贵连接、进程池、容器,且只读使用module / session
某目录下所有测试都要换环境/换配置该目录 conftest.py 里覆盖同名 fixture
全局横切行为(切目录、环境变量、禁网)conftest + autouse=True

7.6 本章小结

  • scope 五档:function/class/module/package/session,决定实例创建与 teardown 时机;
  • 高作用域省时间但共享状态,只读资源才适合提升作用域;
  • conftest 分层 + 同名就近覆盖 = 项目级默认与局部定制的标准模式;
  • autouse=True 提供无需声明的横切 setup/teardown,要克制;
  • session/module fixture 不能依赖更低作用域的 fixture(ScopeMismatch)。

🧪 随堂测验

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

1. scope="module" 的 fixture 在什么时刻执行 teardown?

2. session 作用域的 fixture 依赖了 function 作用域的 fixture,会发生什么?

3. tests/api/conftest.py 与 tests/conftest.py 定义了同名 fixture api_client,tests/api/ 下的测试会拿到哪个?

4. 以下哪种用法最适合 autouse?

🛠️ 动手实践

  1. 把第 6 章 db_conn fixture 分别改成 module 和 session 作用域,插入数据后写第二个测试验证"能看到第一个测试的数据",体会共享状态的双刃剑,最后恢复 function 并解释为什么。
  2. 在项目根 conftest.py 定义 app_mode = "prod",再在某子目录 conftest.py 覆盖为 "test",写一个打印该值的测试确认就近覆盖生效。
  3. 编写一个 autouse fixture:记录每个测试的开始/结束时间戳并在 teardown 打印耗时,跑完整个测试套件观察输出。

至此 fixture 的核心心智模型已完整,下一章进入工厂模式、finalizer 与 request 对象等高级技巧。