
在上一篇文章中,我们剖析了 pytest 的整体架构和 pluggy 插件系统。今天,我们把目光聚焦在 pytest 最核心、最强大的功能之一——fixture 上。
你一定写过这样的代码:
import pytest@pytest.fixturedef api_client():client = requests.Session()yield clientclient.close()def test_get_user(api_client):response = api_client.get("/users/1")assert response.status_code == 200
这个 @pytest.fixture 装饰器背后发生了什么?pytest 是如何发现你的 fixture 函数、解析依赖关系、按正确顺序调用的?为什么 yield 之后的代码能保证在测试结束后执行?
本文将深入 pytest 源码,一步步揭开 fixture 机制的神秘面纱。

一、Fixture 的核心概念回顾
在深入源码之前,先明确 fixture 的几个核心特性(也是我们将在源码中追踪的关键点):
| 特性 | 说明 |
|---|---|
| 依赖注入 | 测试函数通过参数名声明需要的 fixture,pytest 自动查找并提供 |
| 作用域 | function、class、module、package、session,控制生命周期 |
| 自动清理 | 使用 yield 实现 teardown,无论测试成功或失败都会执行 |
| 依赖解析 | fixture 可以依赖其他 fixture,pytest 自动构建调用图并按拓扑顺序执行 |
| 缓存机制 | 同一作用域内,fixture 的返回值会被缓存,多次请求不会重复执行 |
| 工厂模式 | 通过 params 参数实现参数化 fixture,每个参数值生成一个独立实例 |
下面我们从 @pytest.fixture 装饰器的源码入口开始。

二、装饰器的魔法:@pytest.fixture 做了什么
2.1 入口:fixture 装饰器
源码位置:_pytest/fixtures.py
def fixture(fixture_function: Optional[Callable] = None,*,scope: Union[_Scope, Callable[[str, Config], _Scope]] = "function",params: Optional[Iterable[object]] = None,autouse: bool = False,ids: Optional[Union[Iterable[Union[str, float, int, bool, None]], Callable[[Any], Optional[object]]]] = None,name: Optional[str] = None,) -> Union[Callable, FixtureFunctionMarker]:
当我们在代码中写 @pytest.fixture 时,实际上是在调用这个函数。它返回一个 FixtureFunctionMarker 对象,这个对象的 __call__ 方法会被装饰器调用,从而对原函数进行“标记”。
2.2 FixtureFunctionMarker 的核心作用
FixtureFunctionMarker 是一个标记类,它不会修改原函数的行为,而是为函数附加元数据——这些元数据存储在 func._pytestfixturefunction 属性中,包含了作用域、autouse、params 等信息。
# 简化版逻辑class FixtureFunctionMarker:def __init__(self, scope, params, autouse, name):self.scope = scopeself.params = paramsself.autouse = autouseself.name = namedef __call__(self, function):# 将 fixture 元数据附加到函数对象上function._pytestfixturefunction = selfreturn function
这样,当 pytest 在收集阶段扫描模块时,就能通过检查 hasattr(obj, "_pytestfixturefunction") 来识别哪些函数是 fixture。
💡 这种“通过附加属性来标记”的设计在 pytest 中非常普遍,也是 Python 装饰器的一种经典用法——不改变函数行为,只添加元信息。

三、Fixture 的收集:pytest 如何发现 fixture
3.1 收集入口:pytest_collection 阶段
在测试收集阶段,pytest 会遍历每个测试模块,对于模块中定义的每个函数,调用 _pytest.python.pycollector 来判断它是普通测试函数还是 fixture。
关键代码位于 _pytest/python.py 中的 PyCollector 和 pytest_pycollect_makemodule 钩子。
3.2 Fixture 注册到 session._fixturemanager
所有收集到的 fixture 会被注册到 session._fixturemanager(一个 FixtureManager 实例)。FixtureManager 是整个 fixture 系统的中央注册表,它负责:
存储所有已知 fixture 的定义(函数、作用域、参数等)
解析 fixture 之间的依赖关系
按作用域缓存 fixture 的实例
# 简化版 FixtureManager 核心结构class FixtureManager:def __init__(self, session):self.session = sessionself._arg2fixturedefs = {} # 参数名 -> FixtureDef 列表self._fixturedefs = [] # 所有 FixtureDef 对象self._matchings = {} # 缓存节点与 fixture 的匹配关系
每个 fixture 会被包装成一个 FixtureDef 对象,它包含了执行 fixture 所需的所有信息:函数引用、作用域、参数、依赖的 fixture 名称列表等。
3.3 autouse fixture 的特殊处理
autouse=True 的 fixture 会被特殊标记。在收集阶段结束时,FixtureManager 会扫描所有 autouse fixture,并将它们自动添加到每个匹配的测试节点上。这就是为什么 autouse fixture 无需在测试参数中声明也能生效。

四、依赖解析:pytest 如何知道先调谁
当测试函数有多个 fixture 参数时,pytest 需要按正确顺序调用这些 fixture。更重要的是,fixture 本身可能依赖其他 fixture,这就形成了一个有向无环图(DAG)。
4.1 依赖图的构建
FixtureDef 对象在初始化时会解析其依赖:
class FixtureDef:def __init__(self, func, scope, params, ...):self.func = funcself.scope = scope# 解析函数参数,这些参数名就是依赖的 fixture 名称self.argnames = getfixturenames(func, self)
getfixturenames 会检查函数的签名(inspect.signature),提取所有参数名,过滤掉非 fixture 的参数(如 self、cls),剩下的就是依赖的 fixture 名称。
4.2 拓扑排序:确定执行顺序
当 pytest 需要为一个测试函数准备所有依赖时,会调用 FixtureManager.getfixturedefs 递归地获取每个依赖的 FixtureDef,然后通过 _compute_fixture_deps 进行拓扑排序。
核心逻辑伪代码:
def _compute_fixture_deps(fixturedef, fixturemanager, seen):# 深度优先遍历依赖图for argname in fixturedef.argnames:dep_fixturedef = fixturemanager.getfixturedefs(argname, ...)[-1]if dep_fixturedef not in seen:_compute_fixture_deps(dep_fixturedef, fixturemanager, seen)seen.append(fixturedef)return seen # 按依赖顺序返回
这个递归函数确保了:被依赖的 fixture 会先执行,依赖方后执行。
4.3 循环依赖检测
如果出现了 A 依赖 B、B 依赖 A 的情况,pytest 会在依赖解析阶段抛出异常:
pytest.fixtures.FixtureLookupError:RecursionError: A fixture named 'a' tried to use itself recursively.
源码中通过一个 _active_fixture_stack 来检测循环:每次进入一个 fixture 的求值前,会检查它是否已经在当前调用栈中。

五、请求上下文:request 对象和 FixtureRequest
细心的你一定注意到,fixture 函数可以接受一个特殊的 request 参数:
@pytest.fixturedef user_id(request):return request.param # 用于参数化 fixture
这个 request 对象是 FixtureRequest 的实例,它封装了当前请求的上下文信息,包括:
| 属性/方法 | 说明 |
|---|---|
request.param | 参数化 fixture 的当前参数值 |
request.node | 当前测试节点 (Item) |
request.config | pytest 配置对象 |
request.module | 当前测试模块 |
request.cls | 当前测试类(如果存在) |
request.getfixturevalue(name) | 动态获取其他 fixture 的值 |
FixtureRequest 对象在 pytest 准备执行一个测试节点时创建,并传递给该节点所需的所有 fixture。
5.1 getfixturevalue 的内部实现
这个方法是动态获取 fixture 的关键。它的执行流程:
通过 _get_active_fixturedef 找到对应参数的 FixtureDef
调用 FixtureDef.execute(request) 获取或创建 fixture 实例
返回实例值
这意味着你可以在 fixture 中根据运行时条件来决定获取哪些其他 fixture——非常灵活。

六、作用域管理与缓存机制
6.1 作用域的定义
pytest 内置五种作用域:function、class、module、package、session。
FixtureDef 中存储了作用域信息,并在创建实例时,会从 FixtureManager 获取一个作用域缓存字典:
def _get_scope_cache(self, request):scope = self._get_scope(request)if scope == "function":return request._func_request._fixture_cache # 每个测试函数独立elif scope == "class":return request.cls._pytest_fixture_cache # 每个测试类共享elif scope == "module":return request.module._pytest_fixture_cache # 每个模块共享elif scope == "session":return request.session._sessioncache # 整个会话共享
6.2 缓存的键与命中逻辑
缓存字典的键是 (fixturedef, params_tuple),其中 params_tuple 用于区分参数化 fixture 的不同参数值。当同一个 fixture 在同一作用域被多次请求时,只需从缓存中取出已创建的值,不会重复执行 fixture 函数。
这个机制极大地提升了性能:比如 scope="session" 的数据库连接 fixture,只会被创建一次,所有测试共享同一个连接。
6.3 yield fixture 的缓存特殊处理
对于使用 yield 的 fixture,pytest 在第一次请求时会执行到 yield 处,将 yield 后的代码暂存起来,然后返回值。当作用域结束时(比如 function 作用域的 fixture 在测试函数执行完毕后),pytest 会调用缓存中保存的 teardown 代码。
这个 teardown 的调用时机由 FixtureDef.finish() 方法控制,它会在节点的 teardown 阶段被执行。

七、参数化 fixture:@pytest.fixture(params=...)
当你这样写时:
@pytest.fixture(params=["chrome", "firefox", "safari"])def browser(request):return request.param
pytest 会为每个参数值生成一个独立的 fixture 实例。在源码层面,这通过 FixtureDef.cached_result 存储为一个列表,每个参数值对应一个缓存条目。
参数化 fixture 和测试函数的参数化可以叠加使用,pytest 会自动处理它们的笛卡尔积。

八、完整执行链路图
让我们用一张图总结 fixture 从收集到执行的全过程:
1. 测试文件被加载│▼2. 扫描模块中的函数,识别 @pytest.fixture 装饰器│ -> 创建 FixtureFunctionMarker 附加元数据│ -> 注册到 FixtureManager (创建 FixtureDef)│▼3. 收集测试函数,解析其参数列表│ -> 参数名 -> 去 FixtureManager 查找对应的 FixtureDef│ -> 递归解析依赖,构建依赖图(拓扑排序)│▼4. 执行测试(调用 pytest_runtest_call)│├─ 为每个需要的 fixture 调用 FixtureDef.execute()│ ├─ 检查作用域缓存,命中则直接返回│ ├─ 未命中则递归执行依赖的 fixture(先执行依赖)│ ├─ 调用 fixture 函数(传入 request)│ └─ 缓存结果(如果使用 yield,注册 teardown 回调)│└─ 执行测试函数主体│▼5. 测试完成后,调用注册的 teardown 回调(yield 后的代码)
关键数据结构一览
| 类 | 职责 |
|---|---|
FixtureManager | 全局注册表,管理所有 fixture 定义,提供依赖解析 |
FixtureDef | 单个 fixture 的定义,包含函数、作用域、参数、依赖列表、缓存 |
FixtureRequest | 请求上下文,封装了当前测试节点信息,提供动态获取 fixture 的能力 |
_FixtureCaching | 作用域级别的缓存存储,位于 request/session/module/class 上 |

九、实战:调试 fixture 源码
如果你想在源码中调试 fixture 的执行过程,可以在以下位置设置断点:
_pytest/fixtures.py:FixtureDef.execute —— fixture 实际执行的地方
_pytest/fixtures.py:FixtureManager.getfixturedefs —— 获取 fixture 定义
_pytest/python.py:Function._requestfixture —— 测试函数请求 fixture 的入口
建议使用 pytest --trace 或在代码中插入 breakpoint() 结合 pdb 单步跟踪。

十、总结与延伸思考
通过源码分析,我们揭示了 pytest fixture 背后的一系列精妙设计:
标记而非修改:@pytest.fixture 只是添加元数据,不改变原函数行为
集中注册:FixtureManager 作为中央注册表,管理所有 fixture 定义
依赖图 + 拓扑排序:自动解析依赖关系,按正确顺序执行
作用域缓存:不同粒度的缓存机制,提升性能
请求对象:封装上下文,支持动态获取和参数化
yield 语法:利用生成器实现 teardown,优雅且可靠

商务合作:RYXtest
夜雨聆风