一句话结论:yield 做清理、fixture 嵌套 3 层、params 一行生成测试矩阵、scope 控制初始化频率、conftest.py 自动共享------5 个操作让你的测试代码少写一半。
📊 先看对比
| 维度 | 只会 return |
掌握 5 个操作后 |
|---|---|---|
| setup/teardown | 两个函数分开写 | 一个 yield 搞定 |
| 多层依赖 | 全揉在一个 fixture 里 | 各管一层,清晰可复用 |
| 多场景测试 | 手写 N 个测试函数 | 一行 params 自动生成 |
| 初始化次数 | 每个测试都重新初始化 | scope 精确控制 |
| fixture 共享 | 每个文件手动 import | conftest.py 自动发现 |
一、只会 return 的日子
三年前我写 pytest,fixture 只有一个用法:
python
@pytest.fixture
def db():
return create_connection()
直到有一天------3000 条测试数据残留,因为我不知道怎么写 teardown。
pytest 的 fixture 比 unittest 的 setup/teardown 强十倍。 花了三年,我把玩法升级到了 5 个等级。
二、骚操作 ①:yield ------ setup + teardown 一锅端
不用把 setup 和 teardown 写两个函数。yield 前面的代码是 setup,后面的代码是 teardown:
python
@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 保证一定清理:
python
@pytest.fixture
def temp_file():
path = "/tmp/test.json"
try:
yield path
finally:
os.remove(path) # 测试失败也会执行
三、骚操作 ②:嵌套 ------ fixture 调 fixture
一个测试要 db → user → order,不用全揉在一起:
python
@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()
测试函数:
python
def test_cancel(order): # 只声明最上层,pytest 自动注入依赖链
order.cancel()
assert order.status == "cancelled"
3 层依赖,改哪层动哪层,互不干扰。
⚠️ 踩坑:别写循环依赖。
a(b)和b(a)同时存在 → pytest 直接报错。
四、骚操作 ③:参数化 ------ 一行代码生成测试矩阵
3 种支付 × 2 种用户 = 6 个用例?不用手写 6 个函数:
python
@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
输出:
css
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 自定义中文名:
python
@pytest.fixture(params=[
("wechat", 100),
("alipay", 0.01),
], ids=["微信-正常", "支付宝-最小"])
五、骚操作 ④:scope ------ 控制初始化频率
db 每个测试都重建 → 300 个测试建 300 次连接。加 scope:
python
@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:
python
@pytest.fixture(scope="session")
def db():
conn = create_db()
yield conn
conn.close()
tests/integration/test_payment.py:
python
def test_pay(api_client): # 不用 import!pytest 自动找到
resp = api_client.post("/pay", {"amount": 100})
assert resp.status_code == 200
子目录继承父目录的 conftest,反过来不行。
七、速查表
| # | 操作 | 一句话 |
|---|---|---|
| ① | yield | setup + teardown 写一起 |
| ② | 嵌套 | fixture 调 fixture,各管一层 |
| ③ | 参数化 | params 一行生成测试矩阵 |
| ④ | scope | 精确控制初始化频率 |
| ⑤ | conftest | 不 import,自动共享 |
💬 你的 fixture 现在用到第几个骚操作了?评论区报个数。
📌 下一篇预告:《GitHub Actions 自动化测试→部署一条龙》--- push 代码,全自动跑。
🛰️ 公众号:测开实战派 | 专注测试开发 × DevOps 实战分享
🏷️ 标签:Python · pytest · 测试 · 自动化测试
2026 年 6 月 | 约 2500 字 | 预计阅读 6 分钟