第 6 章 · 06 fixture 基础
本章目标:理解 fixture 的依赖注入模型,学会声明 fixture、按参数名注入、用
yield实现 setup/teardown,并知道什么时候该用它替代传统的 setup 函数。
6.1 fixture 到底是什么
fixture 是 pytest 的**依赖注入(DI)**机制:测试函数只要把某个名字写进参数表,pytest 就会找到同名的 fixture 函数,执行它,并把返回值注入进来。官方文档用一个手工等价过程说得很直白——fixture 就是"框架替你调用并传参的工厂函数":
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
@pytest.fixture
def fruit_bowl():
# Arrange:准备资源
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# 只需在参数表里"要",pytest 自动执行 fixture 并注入结果
salad = [f for f in fruit_bowl if f.cubed]
assert all(f.name for f in fruit_bowl)它本质上等价于你手写:
bowl = fruit_bowl() # 框架替你做的
test_fruit_salad(fruit_bowl=bowl)好处立竿见影:测试只声明需要什么,不关心怎么构造。同一个 fixture 可以被几十个测试共享;构造逻辑变了只需改一处。
与 unittest setup 的本质区别
setUp 是继承式的、每个测试类一套、无法复用到别的类;fixture 是按名字组合式的,任何测试、任何类都能按需引用任意多个 fixture,且 fixture 之间还能继续互相依赖。
6.2 yield:一个函数写完 setup 和 teardown
很多资源需要"用完清理"。在 fixture 里用 yield 把代码切成两半:yield 之前是 setup,之后是 teardown:
import sqlite3
import pytest
@pytest.fixture
def db_conn():
# ---- setup ----
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
yield conn
# ---- teardown:测试结束后才执行 ----
conn.close()
def test_insert_user(db_conn):
db_conn.execute("INSERT INTO users (name) VALUES ('Alice')")
(count,) = db_conn.execute("SELECT COUNT(*) FROM users").fetchone()
assert count == 1每个用到 db_conn 的测试都会得到一个全新的内存数据库——这就是 fixture 解决的第一个大问题:测试间的状态隔离。
teardown 是否执行与测试成败无关:即使断言挂了或抛了异常,yield 之后的代码依然会运行,因此清理逻辑是可靠的。
6.3 conftest.py:让 fixture 跨文件共享
fixture 默认只在定义它的模块内可见。跨文件共享的标准位置是 conftest.py——pytest 会在收集时自动加载目录树上的所有 conftest,里面的 fixture 对同级及子目录的所有测试可见:
tests/
├── conftest.py # 全项目共享的 fixture 放这里
├── test_user.py # 可以直接用 db_conn
└── api/
├── conftest.py # 仅 api/ 目录下的测试可见的 fixture
└── test_api.py# tests/conftest.py
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "age": 30}# tests/test_user.py —— 无需任何 import!
def test_adult(sample_user):
assert sample_user["age"] >= 18注意:使用 conftest 里的 fixture 不需要 import,这是 pytest 按名字查找的魔法。代价是 IDE 可能提示"未定义参数",装上 pytest 插件即可识别。
6.4 fixture 之间相互依赖
fixture 的参数表同样可以"要"其他 fixture,pytest 会自动解析整棵依赖树。这让你能把复杂准备拆成简单步骤的组合:
import pytest
@pytest.fixture
def user():
return {"name": "Bob", "orders": []}
@pytest.fixture
def user_with_order(user):
# 依赖另一个 fixture,拿到的是同一个 user 实例
user["orders"].append({"id": 1, "amount": 99})
return user
def test_order_exists(user_with_order):
assert len(user_with_order["orders"]) == 1
def test_user_name_still_there(user_with_order):
assert user_with_order["name"] == "Bob"别过度链接
依赖链太长会让"这个值到底是谁改的"变得难排查。一般建议单条链不超过两三层,超过时考虑直接合并成一个 fixture。
6.5 fixture vs 手动准备:怎么选
| 场景 | 建议 |
|---|---|
| 简单一行数据且只有一个测试用 | 直接写在测试里 |
| 多个测试共用同样的准备/清理 | 抽成 fixture |
| 需要"每测试全新实例"保证隔离 | fixture + yield teardown |
| 需要在所有测试间共享昂贵资源(连接池) | fixture + scope="session"(下一章) |
判断口诀:重复两次就抽 fixture,出现清理逻辑必须用 fixture。
6.6 本章小结
- fixture = 声明式依赖注入:参数名即契约,pytest 负责查找、执行、注入;
yield前为 setup、后为 teardown,无论测试成败都会清理;conftest.py自动加载,其中的 fixture 无需 import 即可被同级及子目录测试使用;- fixture 可嵌套依赖其他 fixture,但链路宜短;
- 复用与隔离需求一旦出现,就该从"测试内手写准备"迁移到 fixture。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 测试函数 def test_a(cart): 中,cart 是如何获得值的?
2. 关于 fixture 中的 yield,正确的是?
3. conftest.py 中定义的 fixture,如何在测试中使用?
4. 以下哪种情况最应该抽取 fixture?
🛠️ 动手实践
- 为
ShoppingCart类编写一个empty_cartfixture(yield 一个空车),再写一个cart_with_itemsfixture 依赖前者塞入 3 件商品;分别写 2 个测试验证。 - 用临时文件的思路实现
log_filefixture:setup 创建空文件,teardown 打印并删除该文件,测试中向文件写入一行再断言内容存在。 - 把第 5 章参数化练习中的
parse_iso_date测试改造为使用 conftest.py 共享的 fixture 提供合法日期样例表。
单个 fixture 已经够用了?下一章我们解锁作用域与共享的完整玩法。