第4章:pytest固件(Fixture)——从入门到精通
说实话,我刚接触pytest那会儿,最让我头疼的就是Fixture。当时我还在想,这不就是setup和teardown换个马甲吗?后来踩了不少坑才明白——Fixture才是pytest的灵魂。今天咱们就把它彻底搞明白。
4.1 什么是Fixture?为什么需要它?
Fixture,说白了就是测试的"前置条件"和"后置清理"。比如你要测试一个数据库操作,总得先连上数据库吧?测试完了总得断开连接吧?这就是Fixture干的事。
我习惯把Fixture理解成"测试的脚手架"。没有它,你的测试代码会变得又臭又长。举个例子:
# 没有Fixture的写法
def test_database():
db = connect_to_database()
result = db.query("SELECT * FROM users")
db.close()
assert len(result) > 0
# 每个测试都要重复写连接和关闭
def test_database_2():
db = connect_to_database()
result = db.query("SELECT * FROM orders")
db.close()
assert len(result) > 0
你看,重复代码满天飞。用Fixture就清爽多了:
import pytest
@pytest.fixture
def db():
# setup:连接数据库
db = connect_to_database()
yield db # 把db对象交给测试函数
# teardown:关闭数据库
db.close()
def test_database(db):
result = db.query("SELECT * FROM users")
assert len(result) > 0
def test_database_2(db):
result = db.query("SELECT * FROM orders")
assert len(result) > 0
嗯,这才是优雅的写法。代码量减半,可读性翻倍。
4.2 Fixture的作用域:控制生命周期
Fixture的作用域决定了它什么时候创建、什么时候销毁。我刚开始做项目时,所有Fixture都用默认作用域,结果跑1000个测试花了半小时——每个测试都重新连接数据库,你说慢不慢?
pytest支持四种作用域:
| 作用域 | 说明 | 适用场景 |
|---|---|---|
| function | 每个测试函数执行一次(默认) | 临时文件、独立数据 |
| class | 每个测试类执行一次 | 类级别的共享资源 |
| module | 每个模块执行一次 | 模块级别的配置加载 |
| session | 整个测试会话只执行一次 | 数据库连接、全局配置 |
用法很简单,加个参数就行:
@pytest.fixture(scope="session")
def db_connection():
"""整个测试过程只连接一次数据库"""
print("连接数据库...")
conn = create_connection()
yield conn
print("关闭数据库连接...")
conn.close()
@ pytest.fixture(scope="function")
def test_data(db_connection):
"""每个测试独立的数据"""
data = generate_test_data()
yield data
clear_test_data(data)
4.3 yield实现teardown:优雅的清理机制
你可能会问:为什么用yield而不是return?其实这就是pytest的巧妙之处。yield之前的代码是setup,yield之后的代码是teardown。一个函数搞定两件事。
我曾经见过有人这么写:
@pytest.fixture
def temp_file():
file = create_temp_file()
return file
def cleanup_temp_file():
delete_temp_file()
然后每个测试都要手动调用cleanup。这不是给自己找麻烦吗?用yield多省心:
@pytest.fixture
def temp_file():
file = create_temp_file()
yield file # 测试执行到这里
delete_temp_file() # 测试结束后自动清理
def test_write_file(temp_file):
temp_file.write("hello")
assert temp_file.read() == "hello"
# 测试结束,自动删除临时文件
@pytest.fixture
def safe_temp_file():
try:
file = create_temp_file()
yield file
finally:
delete_temp_file() # 无论如何都会执行
4.4 autouse:自动生效的Fixture
有些Fixture,你希望每个测试都自动使用,比如打印日志、设置环境变量。这时候autouse就派上用场了。
我记得有一次,测试环境需要设置一个全局的时区。如果每个测试都手动引入Fixture,那代码就太丑了。用autouse完美解决:
@pytest.fixture(autouse=True)
def set_timezone():
"""自动设置测试时区"""
old_tz = os.environ.get('TZ')
os.environ['TZ'] = 'Asia/Shanghai'
time.tzset()
yield
if old_tz:
os.environ['TZ'] = old_tz
else:
del os.environ['TZ']
time.tzset()
def test_time():
# 不需要显式传入set_timezone
assert time.tzname[0] == 'CST'
4.5 Fixture参数化:一次定义,多次使用
这是Fixture最强大的功能之一。说白了,就是让同一个Fixture返回不同的数据,测试函数自动执行多次。
我做过一个支付系统的测试,需要测试不同金额、不同币种的场景。用参数化Fixture,代码量减少了80%:
@pytest.fixture(params=[
{"amount": 100, "currency": "CNY"},
{"amount": 50, "currency": "USD"},
{"amount": 200, "currency": "EUR"},
])
def payment_data(request):
"""参数化的支付数据"""
return request.param
def test_payment(payment_data):
"""自动执行3次,每次传入不同的数据"""
result = process_payment(payment_data)
assert result["status"] == "success"
你想想看,如果没有参数化,你得写3个几乎一模一样的测试函数。现在一个函数搞定,而且新增测试数据只需要在params里加一行。
更高级的用法是组合参数化:
@pytest.fixture(params=["local", "staging", "production"])
def environment(request):
return request.param
@ pytest.fixture(params=["mysql", "postgresql", "sqlite"])
def database(request):
return request.param
def test_database_connection(environment, database):
"""测试3x3=9种组合"""
conn = connect(environment, database)
assert conn.is_connected()
4.6 实战:一个完整的Fixture示例
说了这么多,咱们来个综合案例。假设你要测试一个用户管理系统:
import pytest
import tempfile
import json
# session级别:整个测试只创建一次
@pytest.fixture(scope="session")
def app_config():
return {
"db_url": "sqlite:///test.db",
"debug": True,
"secret_key": "test-key"
}
# module级别:每个模块创建一次临时数据库
@pytest.fixture(scope="module")
def database(app_config):
db = create_database(app_config["db_url"])
db.create_tables()
yield db
db.drop_tables()
db.close()
# function级别:每个测试创建独立用户
@pytest.fixture
def test_user(database):
user = database.create_user(
username="test_user",
email="test@example.com"
)
yield user
database.delete_user(user.id)
# autouse:自动记录测试日志
@pytest.fixture(autouse=True)
def log_test():
print(f"开始测试: {request.node.name}")
yield
print(f"测试结束: {request.node.name}")
# 参数化:测试不同角色
@pytest.fixture(params=["admin", "user", "guest"])
def user_role(request):
return request.param
def test_user_permission(test_user, user_role):
"""测试不同角色的权限"""
test_user.role = user_role
permissions = test_user.get_permissions()
assert len(permissions) > 0
def test_user_email(test_user):
"""测试用户邮箱"""
assert "@" in test_user.email
这个例子涵盖了Fixture的所有核心用法。你仔细看看,每个Fixture各司其职,测试代码干净利落。这就是我为什么说Fixture是pytest的灵魂。
好了,关于Fixture的核心内容就这些。下一章咱们聊聊pytest的断言和异常处理,那又是另一番天地了。