# pytest 的 5 个 fixture 骚操作,我用了 3 年才学会

0 阅读3分钟

一句话结论:yield 做清理、fixture 嵌套 3 层、params 一行生成测试矩阵、scope 控制初始化频率、conftest.py 自动共享——5 个操作让你的测试代码少写一半。


📊 先看对比

维度只会 return掌握 5 个操作后
setup/teardown两个函数分开写一个 yield 搞定
多层依赖全揉在一个 fixture 里各管一层,清晰可复用
多场景测试手写 N 个测试函数一行 params 自动生成
初始化次数每个测试都重新初始化scope 精确控制
fixture 共享每个文件手动 importconftest.py 自动发现

一、只会 return 的日子

三年前我写 pytest,fixture 只有一个用法:

@pytest.fixture
def db():
    return create_connection()

直到有一天——3000 条测试数据残留,因为我不知道怎么写 teardown。

pytest 的 fixture 比 unittest 的 setup/teardown 强十倍。 花了三年,我把玩法升级到了 5 个等级。


二、骚操作 ①:yield —— setup + teardown 一锅端

不用把 setup 和 teardown 写两个函数。yield 前面的代码是 setup,后面的代码是 teardown:

@pytest.fixture
def test_user(db_session):
    # == setup ==
    user = User(name="张三")
    db_session.add(user)
    db_session.commit()

    yield user  # 交给测试函数

    # == teardown(自动执行)==
    db_session.delete(user)
    db_session.commit()

try/finally 保证一定清理:

@pytest.fixture
def temp_file():
    path = "/tmp/test.json"
    try:
        yield path
    finally:
        os.remove(path)  # 测试失败也会执行

三、骚操作 ②:嵌套 —— fixture 调 fixture

一个测试要 db → user → order,不用全揉在一起:

@pytest.fixture
def db():
    conn = create_db()
    yield conn
    conn.close()

@pytest.fixture
def user(db):         # 依赖 db
    u = User(name="张三")
    db.add(u); db.commit()
    yield u
    db.delete(u); db.commit()

@pytest.fixture
def order(db, user):  # 依赖 db + user
    o = Order(user_id=user.id, amount=99)
    db.add(o); db.commit()
    yield o
    db.delete(o); db.commit()

测试函数:

def test_cancel(order):  # 只声明最上层,pytest 自动注入依赖链
    order.cancel()
    assert order.status == "cancelled"

3 层依赖,改哪层动哪层,互不干扰。

⚠️ 踩坑:别写循环依赖。a(b)b(a) 同时存在 → pytest 直接报错。


四、骚操作 ③:参数化 —— 一行代码生成测试矩阵

3 种支付 × 2 种用户 = 6 个用例?不用手写 6 个函数:

@pytest.fixture(params=["wechat", "alipay", "bank_card"])
def payment_method(request):
    return request.param

@pytest.fixture(params=["vip", "normal"])
def user_type(request):
    return request.param

def test_payment(payment_method, user_type):
    """自动生成 3×2=6 个测试"""
    result = pay(method=payment_method, user=user_type)
    assert result.success

输出:

test_payment[wechat-vip]      PASSED
test_payment[wechat-normal]   PASSED
test_payment[alipay-vip]      PASSED
test_payment[alipay-normal]   PASSED
test_payment[bank_card-vip]   PASSED
test_payment[bank_card-normal] PASSED

加上 ids 自定义中文名:

@pytest.fixture(params=[
    ("wechat", 100),
    ("alipay", 0.01),
], ids=["微信-正常", "支付宝-最小"])

五、骚操作 ④:scope —— 控制初始化频率

db 每个测试都重建 → 300 个测试建 300 次连接。加 scope

@pytest.fixture(scope="session")   # 整个测试会话只执行一次
def db():
    conn = create_db()
    yield conn
    conn.close()

@pytest.fixture(scope="module")    # 每个测试文件执行一次
def api_client(app_config):
    return APIClient(app_config)

@pytest.fixture                    # 默认 function,每个测试执行
def fresh_order(db):
    order = Order()
    yield order
    db.delete(order)

四种 scope:

scope时机适用
function每个测试大部分 fixture
class每个测试类类内共享
module每个 .py 文件模块级配置
session整个测试会话数据库连接

⚠️ 踩坑:session 级 fixture 不能依赖 function 级 fixture,会报 ScopeMismatch


六、骚操作 ⑤:conftest.py —— 不 import,自动共享

fixture 放在 conftest.py 里,pytest 自动发现,不需要 import:

tests/
├── conftest.py          ← 全局 fixture
├── unit/
│   ├── conftest.py      ← 只对 unit/ 生效
│   └── test_user.py
└── integration/
    ├── conftest.py      ← 只对 integration/ 生效
    └── test_payment.py

tests/conftest.py

@pytest.fixture(scope="session")
def db():
    conn = create_db()
    yield conn
    conn.close()

tests/integration/test_payment.py

def test_pay(api_client):  # 不用 import!pytest 自动找到
    resp = api_client.post("/pay", {"amount": 100})
    assert resp.status_code == 200

子目录继承父目录的 conftest,反过来不行。


七、速查表

#操作一句话
yieldsetup + teardown 写一起
嵌套fixture 调 fixture,各管一层
参数化params 一行生成测试矩阵
scope精确控制初始化频率
conftest不 import,自动共享

💬 你的 fixture 现在用到第几个骚操作了?评论区报个数。

📌 下一篇预告:《GitHub Actions 自动化测试→部署一条龙》— push 代码,全自动跑。

🛰️ 公众号:测开实战派 | 专注测试开发 × DevOps 实战分享


🏷️ 标签:Python · pytest · 测试 · 自动化测试

2026 年 6 月 | 约 2500 字 | 预计阅读 6 分钟