Skip to content
返回

Pytest:Python 测试框架入门指南

发布于

在开发深度学习框架 Boning 的过程中,我需要一个强大且灵活的测试框架来支撑项目的测试需求。经过调研,我选择了 pytest 作为测试框架,并基于项目特点(包含单元测试、端到端测试、异步测试、性能基准测试等)选择了一系列插件。

本文将从基础概念开始,介绍 pytest 的核心特性,并重点介绍我在 Boning 项目中引入的插件及其使用场景。

什么是 Pytest?

Pytest 是 Python 生态中最流行、功能最强大的测试框架之一。它支持从简单的单元测试到复杂的功能测试,并且可以与现有的测试套件(如 unittest)无缝集成。

核心优势

核心概念速览

Pytest 使用 Python 原生的 assert 语句,自动发现测试,并提供强大的 fixtures 系统用于管理测试依赖。支持参数化测试(@pytest.mark.parametrize)、标记(markers)和异常测试(pytest.raises())等功能。详细用法可参考 官方文档

常用命令行选项

pytest -v              # 详细输出
pytest -vv             # 更详细的输出
pytest -s              # 显示打印输出
pytest --lf            # 只运行失败的测试
pytest -x              # 运行到第一个失败就停止
pytest -n auto         # 并行运行(pytest-xdist)
pytest --cov=boning    # 显示覆盖率(pytest-cov)

Boning 项目中使用的插件

基于项目需求,我们在 Boning 中引入了以下插件。这些插件覆盖了并行执行、异步测试、覆盖率统计、性能基准测试、失败重试等核心测试场景。

核心测试框架

pytest (>=9.0.1)

Pytest 核心框架,提供基础的测试运行、发现和断言功能。

并行执行与性能

pytest-xdist (>=3.8.0)

用途:并行测试执行,显著提升测试速度

Boning 项目包含大量单元测试和端到端测试,使用 pytest-xdist 可以充分利用多核 CPU,大幅缩短测试时间。

# 自动检测 CPU 核心数并并行运行
pytest -n auto

# 指定并行数
pytest -n 4

配置

[tool.pytest.ini_options]
addopts = ["-n", "auto"]

pytest-codspeed (>=4.2.0)

用途:性能基准测试

深度学习框架的性能至关重要,pytest-codspeed 提供了专业的性能基准测试能力,可以追踪性能回归。

def test_tensor_operation(benchmark):
    result = benchmark(some_expensive_operation)
    assert result is not None

异步测试支持

pytest-asyncio (>=1.3.0)

用途:异步测试支持

Boning 框架中大量使用异步操作,pytest-asyncio 提供了对 async/await 语法的原生支持。

import pytest

@pytest.mark.asyncio
async def test_async_operation():
    result = await async_function()
    assert result == expected

测试覆盖率

pytest-cov (>=7.0.0)

用途:测试覆盖率统计

确保代码质量,追踪测试覆盖率,识别未测试的代码路径。

# 生成覆盖率报告
pytest --cov=boning --cov-report=html

# 在 CI 中检查覆盖率阈值
pytest --cov=boning --cov-fail-under=80

测试稳定性

pytest-timeout (>=2.4.0)

用途:测试超时控制

防止测试挂起,特别是在 CI 环境中。可以设置全局超时或针对特定测试设置超时。

# 全局超时配置
[tool.pytest.ini_options]
timeout = 300  # 5 分钟

# 特定测试超时
@pytest.mark.timeout(60)
def test_slow_operation():
    ...

pytest-rerunfailures (>=16.1)

用途:失败重试

对于网络、IO 或资源竞争相关的测试,自动重试可以减少误报。

# 失败后重试 3 次
pytest --reruns 3

# 重试时延迟
pytest --reruns 3 --reruns-delay 1

pytest-randomly (>=4.0.1)

用途:随机化测试顺序

发现测试间的隐藏依赖,确保测试独立性。使用固定种子可以复现失败的测试顺序。

# 随机顺序运行
pytest --randomly

# 使用固定种子
pytest --randomly-seed=42

开发体验

pytest-instafail (>=0.5.0)

用途:即时失败报告

在长时间运行的测试套件中,立即显示失败信息,无需等待所有测试完成。特别适合并行测试场景。

pytest --instafail

pytest-clarity (>=1.0.1)

用途:改进断言输出

提供更清晰的断言失败信息,特别适合 NumPy 数组、张量等复杂数据结构的比较。

# 自动启用,无需额外配置
def test_array_comparison():
    expected = np.array([1, 2, 3])
    actual = np.array([1, 2, 4])
    assert actual == expected  # 会显示详细的差异信息

Mock 支持

pytest-mock (>=3.15.1)

用途:增强的 Mock 支持

提供 mocker fixture,比标准 unittest.mock 更简洁,自动清理 mock,避免测试间污染。

def test_with_mock(mocker):
    mock_func = mocker.patch('boning.some_module.func')
    mock_func.return_value = 42
    # 测试代码
    assert mock_func.called

高级测试功能

pytest-subtests (>=0.15.0)

用途:子测试支持

允许在单个测试中运行多个子断言,即使部分失败也继续执行,适合复杂的测试场景。

def test_multiple_conditions(subtests):
    for i in range(10):
        with subtests.test(i=i):
            assert i % 2 == 0  # 部分失败不影响其他

文档测试

xdoctest (>=1.3.0)

用途:文档测试

自动执行文档字符串中的示例代码,确保文档与代码同步。

def my_function(x):
    """
    Examples:
        >>> my_function(2)
        4
    """
    return x * 2

插件配置示例

在 Boning 项目中,我们使用 pyproject.toml 进行配置:

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]

addopts = [
    "--strict-markers",
    "--tb=short",
    "-n", "auto",              # pytest-xdist: 自动并行
    "--timeout=300",           # pytest-timeout: 5 分钟超时
    "--instafail",             # pytest-instafail: 即时失败
    "--randomly",              # pytest-randomly: 随机顺序
    "--cov=boning",            # pytest-cov: 覆盖率
    "--cov-report=term-missing",
]

markers = [
    "slow: marks tests as slow",
    "integration: marks tests as integration tests",
    "benchmark: marks tests as benchmark tests",
]

timeout = 300
timeout_method = "thread"

插件生态概览

Pytest 拥有超过 1300+ 个插件,覆盖各种测试场景。完整的插件列表可以在 官方插件列表 查看。

其他值得了解的插件

虽然以下插件未在 Boning 项目中引入,但值得了解:

pytest-repeat

用于重复运行测试,帮助发现间歇性失败的测试:

pytest --count=10 test_flaky.py  # 重复运行 10 次

注意:与 pytest-rerunfailures 不同,pytest-repeat 会重复运行所有测试,而 pytest-rerunfailures 只重试失败的测试。

pytest-html

生成美观的 HTML 测试报告,适合在 CI 中生成报告供查看:

pytest --html=report.html --self-contained-html

内置功能

以下功能已内置在 pytest 核心中,无需额外插件:

最佳实践

1. 测试组织

project/
├── src/
│   └── myproject/
│       └── __init__.py
└── tests/
    ├── __init__.py
    ├── conftest.py          # 共享 fixtures
    ├── test_unit/
    │   └── test_core.py
    └── test_integration/
        └── test_api.py

2. 使用 conftest.py

conftest.py 用于存放共享的 fixtures 和配置:

# tests/conftest.py
import pytest

@pytest.fixture
def client():
    """共享的测试客户端"""
    return TestClient()

3. 测试命名

4. 保持测试独立

每个测试应该能够独立运行,不依赖其他测试的执行顺序或状态。

5. 使用 Fixtures 管理资源

@pytest.fixture
def temp_file(tmp_path):
    """使用 pytest 内置的 tmp_path fixture"""
    file = tmp_path / "test.txt"
    file.write_text("test content")
    return file

总结

Pytest 的插件生态非常丰富,可以根据项目实际需求选择合适的插件组合。通过合理配置和使用,可以构建出高效、稳定的测试体系。

参考资源


建议修改

上一篇
__eq__ 的返回类型与里氏替换原则
下一篇
有理分式分解的一种快捷方法