文章目录

Python 单元测试:pytest fixtures 与参数化

发布于 2026-04-03 01:35:23 · 浏览 8 次 · 评论 0 条

Python 单元测试:pytest fixtures 与参数化

在 Python 开发中,编写可靠的单元测试是保证代码质量的关键。pytest 是目前最流行的测试框架之一,它通过 fixtures(夹具)参数化(parametrize) 功能,极大简化了测试的组织与复用。本文将手把手教你如何高效使用这两个核心特性。


理解 pytest fixtures

Fixture 是一种用于设置测试前置条件提供测试依赖对象的机制。你可以把它想象成一个“测试资源工厂”——每次测试需要数据库连接、临时文件、模拟对象等时,fixture 会自动创建并传递给测试函数。

定义一个 fixture 非常简单:

  1. 编写 一个普通函数,并在其上方加上 @pytest.fixture 装饰器。
  2. 在测试函数的参数列表中声明同名参数,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 的作用域

  1. @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

  1. 在 fixture 函数中使用 yield 返回资源
  2. yield 之后的代码会在测试结束后自动执行
@pytest.fixture
def database_connection():
    conn = create_db_connection()  # 假设这是你的连接函数
    **yield** conn
    conn.close()  # 测试结束后自动关闭

无论测试成功或失败,conn.close() 都会被调用,确保资源释放。


参数化测试:一写多测

当你想用同一段测试逻辑验证多种输入输出组合时,参数化能避免重复代码。pytest 提供 @pytest.mark.parametrize 装饰器实现此功能。

对测试函数进行参数化

  1. 在测试函数上方添加 @pytest.mark.parametrize
  2. 第一个参数是字符串,列出测试函数接收的参数名(用逗号分隔)。
  3. 第二个参数是值的列表,每个元素对应一组输入

例如,测试一个加法函数:

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 会分别用 devprod 配置运行两次。


实战:测试一个用户注册函数

假设你有一个 register_user(username, email) 函数,需验证不同输入的有效性。

步骤如下

  1. 创建一个 fixture 提供数据库模拟对象
@pytest.fixture
def mock_db():
    return MockDatabase()  # 假设这是一个轻量级内存数据库模拟
  1. 参数化用户名和邮箱的合法/非法组合
@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_clientempty_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,即可看到每个参数化测试的独立结果,便于快速定位失败用例。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文