在开发深度学习框架 Boning 的过程中,我需要一个强大且灵活的测试框架来支撑项目的测试需求。经过调研,我选择了 pytest 作为测试框架,并基于项目特点(包含单元测试、端到端测试、异步测试、性能基准测试等)选择了一系列插件。
本文将从基础概念开始,介绍 pytest 的核心特性,并重点介绍我在 Boning 项目中引入的插件及其使用场景。
什么是 Pytest?
Pytest 是 Python 生态中最流行、功能最强大的测试框架之一。它支持从简单的单元测试到复杂的功能测试,并且可以与现有的测试套件(如 unittest)无缝集成。
核心优势
- 简洁的语法:使用简单的
assert语句即可编写测试,无需记忆各种断言方法 - 自动发现:自动发现以
test_开头的文件和函数 - 丰富的断言信息:失败时提供详细的上下文信息,便于调试
- 强大的插件系统:拥有超过 1300+ 个插件,覆盖各种测试场景
- 灵活的 Fixture 系统:优雅地管理测试资源和依赖
核心概念速览
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 核心中,无需额外插件:
- pytest-parametrize - 参数化测试(
@pytest.mark.parametrize) - pytest-cache - 测试缓存
- pytest-faulthandler - 故障处理(Python 3.3+)
最佳实践
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. 测试命名
- 测试文件:
test_*.py - 测试函数:
test_* - 测试类:
Test* - Fixtures:描述性名称
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 的插件生态非常丰富,可以根据项目实际需求选择合适的插件组合。通过合理配置和使用,可以构建出高效、稳定的测试体系。