pytest fixture 的作用域和 yield 清理机制怎么理解?
简化版
fixture 用来给测试提供可复用的前置资源,比如测试数据、数据库连接、临时目录、客户端对象。它支持 function、class、module、package、session 等作用域,也支持用 yield 在测试结束后执行清理逻辑。
详细版
fixture 的核心能力有三点:
- 通过参数名自动注入资源;
- 通过
scope控制创建频率; - 通过
yield管理资源释放。
示例:
@pytest.fixture(scope="function")
def db_session():
session = create_session()
yield session
session.rollback()
session.close()
常见作用域:
function:每个测试函数执行一次,隔离性最好;class:每个测试类执行一次;module:每个测试文件执行一次;session:整个测试会话执行一次,适合昂贵但可共享的资源。
面试要强调:作用域越大,性能可能越好,但隔离性越弱。
完整版教学
一、fixture 要解决的重复初始化问题
真实测试很少只调用一个纯函数。更多时候,测试需要准备用户、订单、数据库、缓存、测试客户端、临时文件。如果每个测试都手写这些准备逻辑,代码会又长又容易不一致。
fixture 就是 pytest 提供的测试资源管理机制。它把“准备资源”和“测试逻辑”拆开,让测试函数只声明自己需要什么:
@pytest.fixture
def user():
return {"id": 1, "name": "Tom"}
def test_user_id(user):
assert user["id"] == 1
这里的 user 参数不是普通参数,而是 pytest 自动注入的 fixture 返回值。
二、fixture 的依赖关系
fixture 之间也可以相互依赖:
@pytest.fixture
def db_session():
return create_session()
@pytest.fixture
def user(db_session):
return create_user(db_session, name="Tom")
测试函数只需要声明 user,pytest 会先创建 db_session,再创建 user。这让测试资源可以分层组织。
不过要小心:依赖层次太深会降低可读性。读测试时,如果要跳五六个 fixture 才知道数据怎么来的,维护体验会很糟。
三、fixture 作用域怎么选
fixture 默认作用域是 function,也就是每个测试函数都会重新执行一次。这种隔离性最好,因为测试之间不会共享可变状态。
如果资源创建成本很高,可以扩大作用域。例如一个只读配置对象可以用 session,一个只读测试客户端可以用 module,但数据库事务、用户状态这类容易变化的资源更适合 function。
常见作用域可以这样理解:
function:每个测试一次,最隔离
class:每个测试类一次
module:每个测试文件一次
package:每个测试包一次
session:整个 pytest 运行过程一次
作用域越大,资源复用越多,测试可能更快;但如果资源会被修改,测试之间就可能互相影响。
四、yield fixture 如何清理资源
fixture 如果只 return,只能创建资源,无法自然表达清理逻辑。yield fixture 可以把资源创建和释放放在一起:
@pytest.fixture
def temp_file():
path = create_temp_file()
yield path
remove_file(path)
yield 前的代码在测试前执行,yield 后的代码在测试结束后执行。即使测试失败,清理逻辑也会执行。
数据库 session 常见写法:
@pytest.fixture
def db_session():
session = SessionLocal()
try:
yield session
finally:
session.rollback()
session.close()
这样每个测试结束后回滚并关闭 session,保证测试之间不互相污染。
五、autouse fixture 要谨慎
pytest 支持 autouse=True,表示测试不声明也会自动执行:
@pytest.fixture(autouse=True)
def clear_cache():
cache.clear()
它适合一些全局清理逻辑,比如每个测试前清空缓存。但它也容易制造隐式行为:测试函数看起来没有依赖,实际却执行了很多逻辑。
所以 autouse 要少用,只用于非常明确、团队都知道的公共行为。其他资源更建议显式声明,让测试依赖一眼可见。
六、conftest.py 的作用
pytest 会自动加载 conftest.py 中定义的 fixture。通常可以把跨多个测试文件复用的 fixture 放在 tests/conftest.py。
例如:
tests/
conftest.py
test_user.py
test_order.py
conftest.py 不需要被 import,pytest 会自动发现。它适合放测试客户端、数据库 session、登录用户等公共 fixture。
但也不要把所有东西都塞进一个巨大的 conftest.py。大型项目可以按目录拆分,让不同模块拥有自己的局部 fixture。
七、常见误区与追问
| 作用域 | 创建次数示例 | 适合资源 |
|---|---|---|
function | 100 个测试创建 100 次 | 可变对象、DB session、用户数据 |
module | 1 个文件创建 1 次 | 只读配置、测试客户端 |
session | 整次运行创建 1 次 | 昂贵且不可变的全局资源 |
yield fixture 生命周期:
创建资源 -> yield 给测试 -> 测试通过或失败 -> 执行 yield 后清理代码
易错点:scope 越大越省时间,但隔离性越弱;测试稳定性通常比省几秒初始化更重要。
第一个误区是用 session 作用域共享可变对象。比如所有测试共用同一个列表、同一个数据库 session,某个测试改了数据,后面的测试就可能莫名失败。
第二个误区是 fixture 名称不清楚。data、obj、resource 这类名字太泛,读测试时很难判断它提供什么。
第三个误区是为了复用而过度抽象。测试代码允许有少量重复,过度抽象反而会让用例意图变模糊。
- 误区:
session作用域一定更高效所以应该优先用。 作用域变大确实减少创建次数,但共享可变状态会让测试顺序依赖和偶发失败更难排查。 - 误区:fixture 名越短越方便。
data、obj、resource这类名字信息量太低,读测试时无法判断它提供的业务语义。 - 误区:
autouse=True可以减少所有显式声明。 autouse 会制造隐式执行逻辑,适合全局清理这类明确行为,不适合隐藏复杂业务准备。 - 追问:
yieldfixture 测试失败时还会清理吗? 会执行yield后面的清理逻辑,因此适合关闭文件、回滚事务、释放临时目录。 - 追问:fixture 之间能不能依赖? 可以,pytest 会按依赖图先解析被依赖 fixture,但层级过深会损害可读性。
- 追问:
conftest.py应该放什么? 适合放跨多个测试文件复用的公共 fixture;大型项目应按目录拆分,避免一个巨大文件承载所有测试魔法。
八、加强记忆
记 fixture:它是测试资源的声明式注入机制;scope 决定资源复用范围,yield 决定资源如何清理。默认选 function 保隔离,只有资源昂贵且不可变时才扩大作用域;显式 fixture 比隐式魔法更容易维护。