Skip to content

第 6 章 · 06 fixture 基础

本章目标:理解 fixture 的依赖注入模型,学会声明 fixture、按参数名注入、用 yield 实现 setup/teardown,并知道什么时候该用它替代传统的 setup 函数。

6.1 fixture 到底是什么

fixture 是 pytest 的**依赖注入(DI)**机制:测试函数只要把某个名字写进参数表,pytest 就会找到同名的 fixture 函数,执行它,并把返回值注入进来。官方文档用一个手工等价过程说得很直白——fixture 就是"框架替你调用并传参的工厂函数":

python
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)

它本质上等价于你手写:

python
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

python
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 对同级及子目录的所有测试可见:

text
tests/
├── conftest.py            # 全项目共享的 fixture 放这里
├── test_user.py           # 可以直接用 db_conn
└── api/
    ├── conftest.py        # 仅 api/ 目录下的测试可见的 fixture
    └── test_api.py
python
# tests/conftest.py
import pytest


@pytest.fixture
def sample_user():
    return {"name": "Alice", "age": 30}
python
# 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 会自动解析整棵依赖树。这让你能把复杂准备拆成简单步骤的组合:

python
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?

🛠️ 动手实践

  1. ShoppingCart 类编写一个 empty_cart fixture(yield 一个空车),再写一个 cart_with_items fixture 依赖前者塞入 3 件商品;分别写 2 个测试验证。
  2. 用临时文件的思路实现 log_file fixture:setup 创建空文件,teardown 打印并删除该文件,测试中向文件写入一行再断言内容存在。
  3. 把第 5 章参数化练习中的 parse_iso_date 测试改造为使用 conftest.py 共享的 fixture 提供合法日期样例表。

单个 fixture 已经够用了?下一章我们解锁作用域与共享的完整玩法。