文章目录
- 一、先准备环境
- [二、从一个最小示例理解 fixture](#二、从一个最小示例理解 fixture)
- [三、为什么 fixture 比传统的 setup/teardown 更常用?](#三、为什么 fixture 比传统的 setup/teardown 更常用?)
- [四、Fixture 执行流程图解](#四、Fixture 执行流程图解)
- [五、最佳实践:将 fixture 放入 `conftest.py`](#五、最佳实践:将 fixture 放入
conftest.py) - [六、深入理解 fixture 的作用域(scope)](#六、深入理解 fixture 的作用域(scope))
- 七、作用域实战示例
- [八、`yield` 的真正价值:优雅的后置清理](#八、
yield的真正价值:优雅的后置清理) - 九、`usefixtures`:只需执行,无需返回值
- [十、参数化:`params` + `ids`,一套逻辑跑多组数据](#十、参数化:
params+ids,一套逻辑跑多组数据) - [十一、构建你的 fixture 思维模型](#十一、构建你的 fixture 思维模型)
- 十二、综合流程图:从请求到清理
一、先准备环境
官方入门文档推荐使用如下命令安装 pytest:
bash
pip install -U pytest
pytest --version
同时,强烈建议在虚拟环境中安装和使用 pytest,以避免依赖冲突(参见 [pytest 官方文档][2])。
二、从一个最小示例理解 fixture
首先创建如下目录结构:
text
demo_pytest_fixture/
└─ tests/
└─ test_basic_fixture.py
文件内容如下:
python
# tests/test_basic_fixture.py
import pytest
@pytest.fixture
def user():
print("\n[fixture] 创建测试用户")
return {"name": "张三", "role": "tester"}
def test_user_name(user):
assert user["name"] == "张三"
def test_user_role(user):
assert user["role"] == "tester"
运行命令:
bash
pytest -s
关键理解点:
test_user_name(user)中的user不是普通参数;- 它是在"请求"一个名为
user的 fixture; - pytest 会自动查找
@pytest.fixture def user(); - 执行该函数,并将其返回值注入到测试函数中。
这正是 pytest 的核心机制:测试函数通过参数声明所需资源,pytest 根据参数名自动解析并注入对应的 fixture 实例(详见 [pytest 文档][1])。
三、为什么 fixture 比传统的 setup/teardown 更常用?
fixture 的优势不仅在于"初始化",更在于它统一解决了三个关键问题:
- 前置准备(Setup)
- 资源共享(Reuse)
- 后置清理(Teardown)
更重要的是,这些能力可以被拆分为多个独立、可复用的小型 fixture。官方将 fixture 定义为"为测试提供可靠且一致上下文的机制",支持通过 return 或 yield 两种方式提供资源(参见 [pytest 文档][3])。
四、Fixture 执行流程图解
下图展示了 fixture 的完整执行模型(可直接复制到 PlantUML 使用):

其核心逻辑是:
- pytest 先解析 fixture 的依赖关系;
- 依次执行依赖项,直到遇到
return或yield; - 将资源对象传递给测试函数;
- 若使用
yield,测试结束后会回溯执行yield后的清理代码。
这正是官方描述的"setup → yield → test → teardown"模型(参见 [pytest 文档][1])。
五、最佳实践:将 fixture 放入 conftest.py
在实际项目中,最常见的方式是将 fixture 定义在 conftest.py 中。
📌 注意:
- 文件名必须为
conftest.py,不可更改;- 无需手动导入,pytest 会自动发现;
- 作用域为当前目录及其所有子目录;
- 若多层目录中存在同名 fixture,优先使用最近的
conftest.py。
目录结构示例:
text
demo_pytest_fixture/
└─ tests/
├─ conftest.py
└─ test_login.py
1)conftest.py
python
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def base_url():
print("\n[session] 初始化 base_url")
return "https://demo.example.com"
@pytest.fixture(scope="function")
def login_user():
print("\n[function] 准备登录账号")
return {"username": "admin", "password": "123456"}
@pytest.fixture
def login_session(base_url, login_user):
print(f"\n[fixture] 模拟登录: {login_user['username']} -> {base_url}")
token = f"token-{login_user['username']}"
yield {
"token": token,
"user": login_user,
"base_url": base_url
}
print("\n[teardown] 退出登录,清理会话")
2)test_login.py
python
# tests/test_login.py
def test_login_success(login_session):
assert login_session["token"].startswith("token-")
def test_login_user_is_admin(login_session):
assert login_session["user"]["username"] == "admin"
运行后你会发现:
test_login.py无需 importlogin_session;- pytest 自动解析依赖链:
login_session→base_url+login_user; - 所有 fixture 按需自动注入。
这就是 conftest.py 的强大之处:提供目录级共享资源,且天然支持依赖注入(参见 [pytest 文档][4])。
六、深入理解 fixture 的作用域(scope)
scope 决定了 fixture 的生命周期和复用范围。
pytest 支持以下五种作用域(按生命周期从长到短):
| Scope | 生命周期 |
|---|---|
session |
整个测试会话期间仅初始化一次 |
package |
当前包内所有测试共享 |
module |
单个 .py 文件内共享 |
class |
单个测试类内共享 |
function |
默认,每个测试函数独享 |
⚠️ 注意:高作用域 fixture 优先于低作用域执行;同一作用域内,按依赖顺序执行(参见 [pytest 文档][5])。
作用域层级关系图(PlantUML)

原则:
- 越靠左(如
session),复用性越强,但隔离性越弱; - 越靠右(如
function),隔离性越好,但开销更大。
七、作用域实战示例
python
# tests/test_scope.py
import pytest
@pytest.fixture(scope="session")
def sess():
print("\nSETUP session")
yield "session"
print("\nTEARDOWN session")
@pytest.fixture(scope="module")
def mod():
print("\nSETUP module")
yield "module"
print("\nTEARDOWN module")
@pytest.fixture(scope="class")
def cls():
print("\nSETUP class")
yield "class"
print("\nTEARDOWN class")
@pytest.fixture(scope="function")
def func():
print("\nSETUP function")
yield "function"
print("\nTEARDOWN function")
class TestDemo:
def test_a(self, sess, mod, cls, func):
assert [sess, mod, cls, func] == ["session", "module", "class", "function"]
def test_b(self, sess, mod, cls, func):
assert True
典型输出:
text
SETUP session
SETUP module
SETUP class
SETUP function
test_a
TEARDOWN function
SETUP function
test_b
TEARDOWN function
TEARDOWN class
TEARDOWN module
TEARDOWN session
从中可清晰看出:
session、module、class各只初始化一次;function每次测试都重新创建;- 清理顺序与初始化顺序相反(LIFO)。
这完全符合官方对 fixture 生命周期的定义(参见 [pytest 文档][5])。
八、yield 的真正价值:优雅的后置清理
yield 不仅用于返回资源,更关键的是提供可靠的清理机制。
示例:
python
# tests/test_yield_demo.py
import pytest
@pytest.fixture
def open_file():
print("\n[setup] 打开文件")
f = open("temp_demo.txt", "w", encoding="utf-8")
yield f
print("\n[teardown] 关闭文件")
f.close()
def test_write_file(open_file):
open_file.write("hello fixture")
assert not open_file.closed
执行流程:
- 执行
yield前的 setup 代码; - 将
f注入测试函数; - 测试结束后,自动回溯执行
yield后的 teardown 代码。
✅ 官方推荐:优先使用
yield方式实现 teardown,因其更简洁、不易遗漏(参见 [pytest 文档][1])。
此外,yield 的执行频率受 scope 控制:
function:每次测试前后执行;class:每个测试类前后执行;module/session:对应粒度前后各执行一次。
九、usefixtures:只需执行,无需返回值
当你不需要 fixture 的返回值,但希望确保其副作用被执行 时,可使用 @pytest.mark.usefixtures。
示例:
python
# tests/test_usefixtures.py
import os
import pytest
@pytest.fixture
def prepare_env(monkeypatch):
print("\n[fixture] 准备测试环境变量")
monkeypatch.setenv("APP_ENV", "test")
yield
print("\n[teardown] 清理测试环境变量")
@pytest.mark.usefixtures("prepare_env")
class TestEnv:
def test_env_value(self):
assert os.getenv("APP_ENV") == "test"
def test_other_case(self):
assert os.getenv("APP_ENV") == "test"
特点:
prepare_env在每个测试方法前自动执行;- 测试函数无法访问其返回值(本例中也无返回值);
- 适用于纯副作用场景,如设置环境变量、启动服务等。
⚠️ 注意:不要将
@pytest.mark.usefixtures用在 fixture 函数自身上,这会导致未定义行为,官方已明确不推荐(参见 [pytest 文档][1])。
十、参数化:params + ids,一套逻辑跑多组数据
示例代码
python
# tests/test_param.py
import pytest
def fake_check_permission(username):
return 200 if username == "admin" else 403
@pytest.fixture(
params=[
{"username": "admin", "expected": 200},
{"username": "guest", "expected": 403},
],
ids=["管理员场景", "访客场景"]
)
def case_data(request):
return request.param
def test_permission(case_data):
result = fake_check_permission(case_data["username"])
assert result == case_data["expected"]
关键点
params:定义多组输入,fixture 会为每组参数执行一次;request.param:获取当前轮次的参数;ids:为每组测试生成可读性更强的标识名,便于调试和报告。
运行后,你会看到:
text
test_permission[管理员场景]
test_permission[访客场景]
✅ 这是 pytest 实现数据驱动测试的标准方式(参见 [pytest 文档][1])。
十一、构建你的 fixture 思维模型
写 fixture 时,建议按以下顺序思考:
-
是否多处复用?
→ 是:提取为 fixture(如登录态、DB 连接、临时目录等)。
-
生命周期需要多长?
- 单测独享 →
function - 类内共享 →
class - 模块共享 →
module - 全局共享 →
session
- 单测独享 →
-
是否需要清理?
- 否 →
return - 是 →
yield
- 否 →
-
测试是否需要返回值?
- 需要 → 作为参数接收
- 仅需副作用 → 用
usefixtures
-
是否要跑多组数据?
- 是 → 使用
params+ids
- 是 → 使用
这套思维,本质上是在利用 pytest fixture 的四大核心能力:
✅ 依赖注入|✅ 作用域复用|✅ yield 清理|✅ 参数化执行(参见 [pytest 文档][1])。
十二、综合流程图:从请求到清理

这张图完整串联了 fixture 的生命周期:
请求 → 查找 → 执行(setup)→ 注入 → 测试 → 执行(teardown)