3. pytest固件(Fixture):从入门到实战
说实话,我刚接触pytest那会儿,最让我头疼的就是测试数据准备和清理。每个测试用例都要重复写setup和teardown,代码又臭又长。直到我真正理解了Fixture——嗯,这玩意儿简直就是测试界的瑞士军刀。
3.1 Fixture概念与作用
Fixture,说白了就是测试的"前置条件"和"后置清理"。你想想看,我们写测试时经常需要:
- 创建数据库连接
- 准备测试数据文件
- 模拟用户登录状态
- 启动某个服务
这些重复劳动,Fixture全帮你包了。它的核心作用就三个:
- 数据准备:测试前把环境搭好
- 资源共享:多个测试用例共用同一份数据
- 自动清理:测试完帮你擦屁股
核心要点:Fixture是pytest的依赖注入机制。测试函数声明需要什么参数,pytest就自动给你注入什么。
来看个最简单的例子:
import pytest
@pytest.fixture
def db_connection():
# 这里假装连接数据库
conn = {"host": "localhost", "port": 3306}
return conn
def test_query(db_connection):
# 直接用,不用自己连
assert db_connection["host"] == "localhost"
我在项目中遇到过不少新手,喜欢在每个测试函数里手动创建连接。结果代码里全是重复的样板代码,维护起来想哭。用Fixture之后,清爽多了。
3.2 scope参数详解
Fixture的scope参数,决定了它的生命周期。我刚开始用的时候也搞混过,后来画了个图才彻底明白。
scope有四个可选值,我整理了个表格方便对比:
| scope值 | 执行时机 | 适用场景 | 我的建议 |
|---|---|---|---|
| function | 每个测试函数执行前后 | 大部分通用场景 | 默认选项,安全可靠 |
| class | 每个测试类执行前后 | 类内测试共享资源 | 适合集成测试 |
| module | 每个.py文件执行前后 | 模块级配置加载 | 注意数据污染 |
| session | 整个测试运行前后 | 全局配置、数据库连接 | 慎用!容易出问题 |
避坑指南:我曾经在项目里把数据库连接的fixture设成了session级别,结果跑完一批测试后数据库状态变了,后面的测试全挂了。scope越大,共享风险越高,一定要想清楚再选。
3.3 yield与teardown
早期的pytest版本,fixture只能做setup。后来加了yield,才真正实现了setup+teardown的完整闭环。
yield的用法其实很简单:
@pytest.fixture
def temp_file():
# setup:创建临时文件
file = open("/tmp/test.txt", "w")
file.write("hello")
file.flush()
# yield:把资源交给测试函数
yield file
# teardown:测试结束后清理
file.close()
import os
os.remove("/tmp/test.txt")
def test_read(temp_file):
content = temp_file.read()
assert content == "hello"
为什么会这样设计?你想想看,yield之前的代码是前置准备,yield之后的代码是后置清理。中间用yield把控制权交给测试函数,测试跑完自动回来执行清理。
我个人习惯把yield和上下文管理器结合使用,代码更优雅:
@pytest.fixture
def managed_file():
with open("/tmp/test.txt", "w") as f:
f.write("hello")
yield f
# 退出with块时自动关闭文件
小技巧:如果teardown逻辑比较复杂,可以用多个yield fixtuer组合。每个只负责一件事,职责单一,调试起来也方便。
3.4 内置fixture(tmpdir, capsys等)
pytest自带了一堆好用的内置fixture,不用白不用。我挑几个最常用的说说:
tmpdir:临时目录管理
测试中经常需要读写文件,用完还得删。tmpdir帮你搞定一切:
def test_file_ops(tmpdir):
# tmpdir是一个临时目录对象
file_path = tmpdir.join("data.txt")
file_path.write("test content")
# 读取验证
assert file_path.read() == "test content"
# 测试结束自动删除,不用操心
我在项目中用tmpdir测试过文件上传功能,每个测试用例都有独立的临时目录,互不干扰,爽得很。
capsys:捕获标准输出
测试命令行工具或者日志输出时,capsys是神器:
def test_print_output(capsys):
print("hello world")
captured = capsys.readouterr()
assert captured.out == "hello world\n"
assert captured.err == ""
还有几个常用的内置fixture,我列个表:
| 内置fixture | 作用 | 典型场景 |
|---|---|---|
| tmpdir | 创建临时目录 | 文件读写测试 |
| tmp_path | 返回pathlib.Path对象 | 现代Python项目 |
| capsys | 捕获stdout/stderr | 命令行工具测试 |
| monkeypatch | 动态修改对象/环境变量 | 模拟外部依赖 |
| request | 获取当前测试上下文 | 动态fixture参数 |
重点提醒:内置fixture不需要import,直接当参数用就行。pytest会自动识别并注入。我第一次用的时候还傻乎乎地去查import路径,后来发现根本不需要。
最后分享一个组合使用的例子,把今天学的知识串起来:
import pytest
@pytest.fixture(scope="module")
def db_config():
"""模块级别的数据库配置"""
config = {"host": "localhost", "port": 3306}
yield config
# 模块测试结束后清理
print("清理数据库配置")
@pytest.fixture
def test_data(tmpdir, db_config):
"""使用内置fixture和自定义fixture"""
data_file = tmpdir.join("test_data.json")
data_file.write(str(db_config))
yield data_file
# 临时文件自动清理
def test_data_loading(test_data):
content = test_data.read()
assert "localhost" in content
你看,fixture之间可以互相依赖,内置的和自定义的混着用,pytest会自动帮你处理好依赖顺序。这就是我为什么说fixture是pytest的灵魂——它让测试代码变得干净、可维护、可复用。
嗯,这一章的内容就到这里。记住一句话:好的fixture设计,能让你的测试代码减少一半。下一章我们聊聊参数化测试,到时候再会。