pytest
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesepytest Best Practices
pytest 最佳实践
Comprehensive guidance for writing maintainable, efficient test suites with pytest, grounded in the official pytest documentation.
基于pytest官方文档,为编写可维护、高效的测试套件提供全面指导。
Project Layout
项目布局
Use the layout with tests outside the application package:
srcpyproject.toml
src/
mypkg/
__init__.py
app.py
tests/
conftest.py
test_app.pyConfigure import mode in (recommended for new projects):
importlibpyproject.tomltoml
[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
testpaths = ["tests"]Install the package in editable mode so tests run against the local source:
bash
pip install -e .采用布局,将测试代码放在应用包外部:
srcpyproject.toml
src/
mypkg/
__init__.py
app.py
tests/
conftest.py
test_app.py在中配置导入模式(推荐新项目使用):
pyproject.tomlimportlibtoml
[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
testpaths = ["tests"]以可编辑模式安装包,确保测试运行时使用本地源码:
bash
pip install -e .Test Discovery Conventions
测试发现约定
- Name test files or
test_*.py*_test.py - Name test functions and methods with a prefix
test_ - Use -prefixed classes (no
Testmethod) to group related tests__init__ - Place shared fixtures and plugins in at the appropriate directory level
conftest.py
- 测试文件命名为或
test_*.py*_test.py - 测试函数和方法以前缀命名
test_ - 使用以开头的类(无
Test方法)来分组相关测试__init__ - 将共享fixtures和插件放在对应目录层级的中
conftest.py
Assertions
断言
Use plain statements — pytest rewrites them for detailed failure messages:
assertpython
def test_addition():
assert 1 + 1 == 2
def test_with_message():
result = compute()
assert result > 0, f"Expected positive, got {result}"Floating-point comparisons — use instead of manual tolerance checks:
pytest.approxpython
def test_floats():
assert 0.1 + 0.2 == pytest.approx(0.3)Exception assertions — use as a context manager:
pytest.raisespython
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
def test_exception_message():
with pytest.raises(ValueError, match=r"invalid value"):
parse_value("bad")Never return a boolean from a test function — pytest ignores return values.
使用普通语句——pytest会重写它们以生成详细的失败信息:
assertpython
def test_addition():
assert 1 + 1 == 2
def test_with_message():
result = compute()
assert result > 0, f"Expected positive, got {result}"浮点数比较——使用替代手动容差检查:
pytest.approxpython
def test_floats():
assert 0.1 + 0.2 == pytest.approx(0.3)异常断言——将作为上下文管理器使用:
pytest.raisespython
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
def test_exception_message():
with pytest.raises(ValueError, match=r"invalid value"):
parse_value("bad")切勿从测试函数返回布尔值——pytest会忽略返回值。
Fixtures
Fixtures
Fixtures are the primary mechanism for test setup and teardown. Define them in for shared use, or directly in test modules for local use.
conftest.pyFixtures是测试前置与后置处理的核心机制。可在中定义以实现共享使用,也可直接在测试模块中定义供本地使用。
conftest.pyBasic fixture
基础Fixture
python
import pytest
@pytest.fixture
def user():
return {"name": "Alice", "role": "admin"}
def test_user_role(user):
assert user["role"] == "admin"python
import pytest
@pytest.fixture
def user():
return {"name": "Alice", "role": "admin"}
def test_user_role(user):
assert user["role"] == "admin"Yield fixtures for teardown (preferred over addfinalizer
)
addfinalizer使用Yield Fixtures进行后置处理(优先于addfinalizer
)
addfinalizerpython
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn # test runs here
conn.close() # teardownpython
@pytest.fixture
def db_connection():
conn = create_connection()
yield conn # 测试在此处运行
conn.close() # 后置处理Fixture scope — choose the broadest scope that is still safe
Fixture作用域——选择最宽泛且安全的作用域
| Scope | Lifetime | Use case |
|---|---|---|
| Per test (default) | Mutable state, cheap to create |
| Per test class | Shared state within a class |
| Per test file | Expensive setup shared across a module |
| Entire test run | Database connections, containers |
python
@pytest.fixture(scope="session")
def app_config():
return load_config("test.env")| 作用域 | 生命周期 | 使用场景 |
|---|---|---|
| 每个测试用例(默认) | 可变状态、创建成本低的资源 |
| 每个测试类 | 类内共享状态 |
| 每个测试文件 | 模块内共享的高成本前置操作 |
| 整个测试运行周期 | 数据库连接、容器等 |
python
@pytest.fixture(scope="session")
def app_config():
return load_config("test.env")Safe fixture structure
安全的Fixture结构
Limit each fixture to one state-changing action, paired with its own teardown. This ensures cleanup runs even when other fixtures fail:
python
@pytest.fixture
def created_user(admin_client):
user = admin_client.create_user(name="test")
yield user
admin_client.delete_user(user) # always runs限制每个Fixture仅执行一次状态变更操作,并配对对应的后置处理。这样即使其他Fixture失败,清理操作仍会运行:
python
@pytest.fixture
def created_user(admin_client):
user = admin_client.create_user(name="test")
yield user
admin_client.delete_user(user) # 始终执行Factory fixtures — for multiple instances in one test
工厂Fixture——用于单个测试中的多个实例
python
@pytest.fixture
def make_order():
orders = []
def _make(product, qty):
order = Order(product=product, qty=qty)
orders.append(order)
return order
yield _make
for o in orders: o.cancel()
def test_two_orders(make_order):
o1 = make_order("book", 1)
o2 = make_order("pen", 5)
assert o1.product != o2.productpython
@pytest.fixture
def make_order():
orders = []
def _make(product, qty):
order = Order(product=product, qty=qty)
orders.append(order)
return order
yield _make
for o in orders: o.cancel()
def test_two_orders(make_order):
o1 = make_order("book", 1)
o2 = make_order("pen", 5)
assert o1.product != o2.productconftest.py
placement
conftest.pyconftest.py
的放置
conftest.py- Root — session-wide fixtures (DB, config)
conftest.py - — fixtures scoped to unit tests only
tests/unit/conftest.py - Fixtures are visible to all tests in the same directory and below
- 根目录——会话级Fixtures(数据库、配置等)
conftest.py - ——仅作用于单元测试的Fixtures
tests/unit/conftest.py - Fixtures对同一目录及子目录下的所有测试可见
Parametrization
参数化
@pytest.mark.parametrize
— avoid duplicate test logic
@pytest.mark.parametrize@pytest.mark.parametrize
——避免重复测试逻辑
@pytest.mark.parametrizepython
@pytest.mark.parametrize("value,expected", [
(2, 4),
(3, 9),
(-1, 1),
])
def test_square(value, expected):
assert square(value) == expectedUse to attach marks to individual cases:
pytest.parampython
@pytest.mark.parametrize("n", [
0,
pytest.param(-1, marks=pytest.mark.xfail(reason="negative not supported")),
])
def test_sqrt(n):
assert sqrt(n) >= 0python
@pytest.mark.parametrize("value,expected", [
(2, 4),
(3, 9),
(-1, 1),
])
def test_square(value, expected):
assert square(value) == expected使用为单个用例添加标记:
pytest.parampython
@pytest.mark.parametrize("n", [
0,
pytest.param(-1, marks=pytest.mark.xfail(reason="negative not supported")),
])
def test_sqrt(n):
assert sqrt(n) >= 0Parametrized fixtures — run entire test sets against multiple configurations
参数化Fixtures——针对多配置运行整套测试
python
@pytest.fixture(params=["sqlite", "postgres"])
def db(request):
return create_db(request.param)python
@pytest.fixture(params=["sqlite", "postgres"])
def db(request):
return create_db(request.param)Markers
标记
Register all custom markers in to prevent typo-silent failures:
pyproject.tomltoml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: requires external services",
"unit: fast, isolated tests",
]Enable strict marker validation to turn unknown markers into errors:
toml
[tool.pytest.ini_options]
addopts = ["--strict-markers"]Apply markers to individual tests, classes, or whole modules:
python
@pytest.mark.slow
def test_heavy_computation(): ...在中注册所有自定义标记,避免拼写错误导致的静默失败:
pyproject.tomltoml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: requires external services",
"unit: fast, isolated tests",
]启用严格标记验证,将未知标记转为错误:
toml
[tool.pytest.ini_options]
addopts = ["--strict-markers"]可将标记应用于单个测试、测试类或整个模块:
python
@pytest.mark.slow
def test_heavy_computation(): ...Module-level
模块级标记
pytestmark = pytest.mark.integration
undefinedpytestmark = pytest.mark.integration
undefinedConfiguration (pyproject.toml
)
pyproject.toml配置(pyproject.toml
)
pyproject.tomlCentralise all pytest settings:
toml
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
"--strict-markers",
"--strict-config",
"-ra", # show summary of all non-passing tests
]
testpaths = ["tests"]
markers = [
"slow: deselect with '-m \"not slow\"'",
"integration: requires live services",
]Enable strict mode for maximum safety on pinned pytest versions:
toml
[tool.pytest.ini_options]
strict = true集中管理所有pytest设置:
toml
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
"--strict-markers",
"--strict-config",
"-ra", # 显示所有非通过测试的摘要
]
testpaths = ["tests"]
markers = [
"slow: deselect with '-m \"not slow\"'",
"integration: requires live services",
]在固定pytest版本时,启用严格模式以获得最高安全性:
toml
[tool.pytest.ini_options]
strict = trueSkipping and Expected Failures
跳过与预期失败
Use for condition-based skips; document the reason:
skipifpython
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX only")
def test_symlinks(): ...Use to document known broken behaviour; add once fixed:
xfailstrict=Truepython
@pytest.mark.xfail(reason="issue #42: parser bug", strict=False)
def test_parser_edge_case(): ...使用进行条件性跳过,并注明原因:
skipifpython
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX only")
def test_symlinks(): ...使用记录已知的故障行为;修复后添加:
xfailstrict=Truepython
@pytest.mark.xfail(reason="issue #42: parser bug", strict=False)
def test_parser_edge_case(): ...monkeypatch — preferred over unittest.mock
for simple patching
unittest.mockmonkeypatch——简单补丁的优先选择(优于unittest.mock
)
unittest.mockpython
def test_env_override(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key")
assert get_api_key() == "test-key"
def test_function_patch(monkeypatch):
monkeypatch.setattr("mymodule.fetch", lambda url: {"ok": True})
assert fetch_data() == {"ok": True}monkeypatchpython
def test_env_override(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key")
assert get_api_key() == "test-key"
def test_function_patch(monkeypatch):
monkeypatch.setattr("mymodule.fetch", lambda url: {"ok": True})
assert fetch_data() == {"ok": True}monkeypatchTemporary Files
临时文件
Use (a ) instead of :
tmp_pathpathlib.Pathtempfilepython
def test_writes_file(tmp_path):
out = tmp_path / "result.txt"
write_report(out)
assert out.read_text() == "done"使用(类型)替代:
tmp_pathpathlib.Pathtempfilepython
def test_writes_file(tmp_path):
out = tmp_path / "result.txt"
write_report(out)
assert out.read_text() == "done"Quick Reference
快速参考
| Goal | Tool |
|---|---|
| Assert equality | |
| Assert float equality | |
| Assert exception raised | |
| Assert warning emitted | |
| Skip conditionally | |
| Document known failure | |
| Run test N times with data | |
| Shared setup/teardown | |
| Patch objects/env | |
| Temp files | |
| 目标 | 工具 |
|---|---|
| 断言相等 | |
| 断言浮点数相等 | |
| 断言异常抛出 | |
| 断言警告触发 | |
| 条件性跳过 | |
| 记录已知故障 | |
| 多数据重复运行测试 | |
| 共享前置/后置处理 | |
| 补丁对象/环境 | |
| 临时文件 | |
Additional Resources
额外资源
Reference Files
参考文档
- — Fixture scopes, autouse, factory pattern, safe teardown patterns
references/fixtures.md - — Full
references/configuration.md/pyproject.tomloption reference and strict mode detailspytest.ini
- —— Fixture作用域、自动使用、工厂模式、安全后置处理模式
references/fixtures.md - —— 完整的
references/configuration.md/pyproject.toml选项参考及严格模式细节pytest.ini