Python 单元测试:pytest fixtures 与参数化
在 Python 开发中,编写可靠的单元测试是保证代码质量的关键。pytest 是目前最流行的测试框架之一,它通过 fixtures(夹具) 和 参数化(parametrize) 功能,极大简化了测试的组织与复用。本文将手把手教你如何高效使用这两个核心特性。
理解 pytest fixtures
Fixture 是一种用于设置测试前置条件或提供测试依赖对象的机制。你可以把它想象成一个“测试资源工厂”——每次测试需要数据库连接、临时文件、模拟对象等时,fixture 会自动创建并传递给测试函数。
定义一个 fixture 非常简单:
- 编写 一个普通函数,并在其上方加上
@pytest.fixture装饰器。 - 在测试函数的参数列表中声明同名参数,pytest 会自动注入该 fixture 的返回值。
例如,创建一个返回固定数值的 fixture:
import pytest
@pytest.fixture
def sample_data():
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
assert sum(sample_data) == 15
运行此测试时,sample_data 会被自动替换为 [1, 2, 3, 4, 5]。
控制 fixture 的作用域
默认情况下,fixture 在每个测试函数执行前都会重新创建。但有时你希望复用同一个实例(比如数据库连接),这时可通过 scope 参数控制生命周期。
设置 fixture 的作用域:
- 在
@pytest.fixture中传入scope参数,可选值包括:"function"(默认):每个测试函数一次"class":每个测试类一次"module":每个.py文件一次"session":整个测试会话一次
例如,创建一个模块级的临时目录:
import tempfile
import pytest
@pytest.fixture(scope="module")
def temp_dir():
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
# cleanup 自动由上下文管理器处理
所有在同一模块中的测试函数共享这个 temp_dir 路径,避免重复创建和删除目录的开销。
使用 fixture 进行清理(teardown)
很多资源(如文件、网络连接)在测试后需要清理。利用 fixture 的生成器语法(yield)可优雅实现 setup/teardown。
实现带清理逻辑的 fixture:
- 在 fixture 函数中使用
yield返回资源。 yield之后的代码会在测试结束后自动执行。
@pytest.fixture
def database_connection():
conn = create_db_connection() # 假设这是你的连接函数
**yield** conn
conn.close() # 测试结束后自动关闭
无论测试成功或失败,conn.close() 都会被调用,确保资源释放。
参数化测试:一写多测
当你想用同一段测试逻辑验证多种输入输出组合时,参数化能避免重复代码。pytest 提供 @pytest.mark.parametrize 装饰器实现此功能。
对测试函数进行参数化:
- 在测试函数上方添加
@pytest.mark.parametrize。 - 第一个参数是字符串,列出测试函数接收的参数名(用逗号分隔)。
- 第二个参数是值的列表,每个元素对应一组输入。
例如,测试一个加法函数:
def add(a, b):
return a + b
@pytest.mark.parametrize("x, y, expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
(100, 200, 300)
])
def test_add(x, y, expected):
assert add(x, y) == expected
pytest 会自动运行 4 次 test_add,每次传入不同的 (x, y, expected) 组合。
结合 fixtures 与参数化
有时你需要根据参数动态生成 fixture。虽然不能直接对 fixture 参数化,但可通过间接方式实现。
方法:让 fixture 接收另一个参数化的输入。
@pytest.fixture
def config(request):
# request 是 pytest 内置 fixture,可访问当前测试的参数
env = request.param
if env == "dev":
return {"host": "localhost", "port": 8000}
elif env == "prod":
return {"host": "api.example.com", "port": 443}
@pytest.mark.parametrize("config", ["dev", "prod"], indirect=True)
def test_api_endpoint(config):
assert "host" in config
assert "port" in config
关键点在于:
- 在
@pytest.mark.parametrize中设置indirect=True。 - fixture 名称必须与参数名一致(这里是
config)。 - fixture 通过
request.param获取实际传入的值。
这样,test_api_endpoint 会分别用 dev 和 prod 配置运行两次。
实战:测试一个用户注册函数
假设你有一个 register_user(username, email) 函数,需验证不同输入的有效性。
步骤如下:
- 创建一个 fixture 提供数据库模拟对象:
@pytest.fixture
def mock_db():
return MockDatabase() # 假设这是一个轻量级内存数据库模拟
- 参数化用户名和邮箱的合法/非法组合:
@pytest.mark.parametrize("username,email,valid", [
("alice", "alice@example.com", True),
("bob123", "bob@test.org", True),
("", "empty@test.com", False), # 用户名为空
("charlie", "", False), # 邮箱为空
("user@name", "user@test.com", False), # 用户名含非法字符
])
def test_register_user(mock_db, username, email, valid):
if valid:
result = register_user(username, email, db=mock_db)
assert result.success is True
assert mock_db.user_exists(username)
else:
with pytest.raises(InvalidInputError):
register_user(username, email, db=mock_db)
此测试覆盖了正常路径和异常路径,且数据库模拟通过 fixture 自动注入,代码清晰无冗余。
常用技巧与最佳实践
- 命名清晰:fixture 名应体现其用途,如
authenticated_client、empty_cart。 - 避免副作用:fixture 应尽量无状态,或确保 teardown 完整。
- 组合使用:一个测试函数可接收多个 fixture,pytest 会按依赖顺序自动解析。
- 调试 fixture:使用
pytest --fixtures命令查看当前可用的所有 fixtures 及其作用域。
| 技巧 | 说明 |
|---|---|
autouse=True |
让 fixture 自动应用于所有测试(无需显式声明) |
params 参数 |
直接在 fixture 上参数化(类似 indirect 但更简洁) |
conftest.py |
将通用 fixtures 放在此文件中,自动被 pytest 发现 |
例如,在 conftest.py 中定义全局 fixture:
# conftest.py
import pytest
@pytest.fixture
def api_client():
return APIClient(base_url="http://localhost:8000")
任何测试文件均可直接使用 api_client,无需导入。
运行测试:在项目根目录执行 pytest -v,即可看到每个参数化测试的独立结果,便于快速定位失败用例。

暂无评论,快来抢沙发吧!