pytest

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

pytest Best Practices

pytest 最佳实践

Comprehensive guidance for writing maintainable, efficient test suites with pytest, grounded in the official pytest documentation.
基于pytest官方文档,为编写可维护、高效的测试套件提供全面指导。

Project Layout

项目布局

Use the
src
layout with tests outside the application package:
pyproject.toml
src/
    mypkg/
        __init__.py
        app.py
tests/
    conftest.py
    test_app.py
Configure
importlib
import mode in
pyproject.toml
(recommended for new projects):
toml
[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 .
采用
src
布局,将测试代码放在应用包外部:
pyproject.toml
src/
    mypkg/
        __init__.py
        app.py
tests/
    conftest.py
    test_app.py
pyproject.toml
中配置
importlib
导入模式(推荐新项目使用):
toml
[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
testpaths = ["tests"]
以可编辑模式安装包,确保测试运行时使用本地源码:
bash
pip install -e .

Test Discovery Conventions

测试发现约定

  • Name test files
    test_*.py
    or
    *_test.py
  • Name test functions and methods with a
    test_
    prefix
  • Use
    Test
    -prefixed classes (no
    __init__
    method) to group related tests
  • Place shared fixtures and plugins in
    conftest.py
    at the appropriate directory level
  • 测试文件命名为
    test_*.py
    *_test.py
  • 测试函数和方法以
    test_
    前缀命名
  • 使用以
    Test
    开头的类(无
    __init__
    方法)来分组相关测试
  • 将共享fixtures和插件放在对应目录层级的
    conftest.py

Assertions

断言

Use plain
assert
statements — pytest rewrites them for detailed failure messages:
python
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
pytest.approx
instead of manual tolerance checks:
python
def test_floats():
    assert 0.1 + 0.2 == pytest.approx(0.3)
Exception assertions — use
pytest.raises
as a context manager:
python
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.
使用普通
assert
语句——pytest会重写它们以生成详细的失败信息:
python
def test_addition():
    assert 1 + 1 == 2

def test_with_message():
    result = compute()
    assert result > 0, f"Expected positive, got {result}"
浮点数比较——使用
pytest.approx
替代手动容差检查:
python
def test_floats():
    assert 0.1 + 0.2 == pytest.approx(0.3)
异常断言——将
pytest.raises
作为上下文管理器使用:
python
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
conftest.py
for shared use, or directly in test modules for local use.
Fixtures是测试前置与后置处理的核心机制。可在
conftest.py
中定义以实现共享使用,也可直接在测试模块中定义供本地使用。

Basic 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
)

使用Yield Fixtures进行后置处理(优先于
addfinalizer

python
@pytest.fixture
def db_connection():
    conn = create_connection()
    yield conn          # test runs here
    conn.close()        # teardown
python
@pytest.fixture
def db_connection():
    conn = create_connection()
    yield conn          # 测试在此处运行
    conn.close()        # 后置处理

Fixture scope — choose the broadest scope that is still safe

Fixture作用域——选择最宽泛且安全的作用域

ScopeLifetimeUse case
function
Per test (default)Mutable state, cheap to create
class
Per test classShared state within a class
module
Per test fileExpensive setup shared across a module
session
Entire test runDatabase connections, containers
python
@pytest.fixture(scope="session")
def app_config():
    return load_config("test.env")
作用域生命周期使用场景
function
每个测试用例(默认)可变状态、创建成本低的资源
class
每个测试类类内共享状态
module
每个测试文件模块内共享的高成本前置操作
session
整个测试运行周期数据库连接、容器等
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.product
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.product

conftest.py
placement

conftest.py
的放置

  • Root
    conftest.py
    — session-wide fixtures (DB, config)
  • tests/unit/conftest.py
    — fixtures scoped to unit tests only
  • Fixtures are visible to all tests in the same directory and below
  • 根目录
    conftest.py
    ——会话级Fixtures(数据库、配置等)
  • tests/unit/conftest.py
    ——仅作用于单元测试的Fixtures
  • Fixtures对同一目录及子目录下的所有测试可见

Parametrization

参数化

@pytest.mark.parametrize
— avoid duplicate test logic

@pytest.mark.parametrize
——避免重复测试逻辑

python
@pytest.mark.parametrize("value,expected", [
    (2, 4),
    (3, 9),
    (-1, 1),
])
def test_square(value, expected):
    assert square(value) == expected
Use
pytest.param
to attach marks to individual cases:
python
@pytest.mark.parametrize("n", [
    0,
    pytest.param(-1, marks=pytest.mark.xfail(reason="negative not supported")),
])
def test_sqrt(n):
    assert sqrt(n) >= 0
python
@pytest.mark.parametrize("value,expected", [
    (2, 4),
    (3, 9),
    (-1, 1),
])
def test_square(value, expected):
    assert square(value) == expected
使用
pytest.param
为单个用例添加标记:
python
@pytest.mark.parametrize("n", [
    0,
    pytest.param(-1, marks=pytest.mark.xfail(reason="negative not supported")),
])
def test_sqrt(n):
    assert sqrt(n) >= 0

Parametrized 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
pyproject.toml
to prevent typo-silent failures:
toml
[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.toml
中注册所有自定义标记,避免拼写错误导致的静默失败:
toml
[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
undefined
pytestmark = pytest.mark.integration
undefined

Configuration (
pyproject.toml
)

配置(
pyproject.toml

Centralise 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 = true

Skipping and Expected Failures

跳过与预期失败

Use
skipif
for condition-based skips; document the reason:
python
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX only")
def test_symlinks(): ...
Use
xfail
to document known broken behaviour; add
strict=True
once fixed:
python
@pytest.mark.xfail(reason="issue #42: parser bug", strict=False)
def test_parser_edge_case(): ...
使用
skipif
进行条件性跳过,并注明原因:
python
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX only")
def test_symlinks(): ...
使用
xfail
记录已知的故障行为;修复后添加
strict=True
python
@pytest.mark.xfail(reason="issue #42: parser bug", strict=False)
def test_parser_edge_case(): ...

monkeypatch — preferred over
unittest.mock
for simple patching

monkeypatch——简单补丁的优先选择(优于
unittest.mock

python
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}
monkeypatch
automatically reverts all changes after the test — no manual cleanup needed.
python
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}
monkeypatch
会在测试结束后自动还原所有修改——无需手动清理。

Temporary Files

临时文件

Use
tmp_path
(a
pathlib.Path
) instead of
tempfile
:
python
def test_writes_file(tmp_path):
    out = tmp_path / "result.txt"
    write_report(out)
    assert out.read_text() == "done"
使用
tmp_path
pathlib.Path
类型)替代
tempfile
python
def test_writes_file(tmp_path):
    out = tmp_path / "result.txt"
    write_report(out)
    assert out.read_text() == "done"

Quick Reference

快速参考

GoalTool
Assert equality
assert a == b
Assert float equality
pytest.approx
Assert exception raised
pytest.raises()
Assert warning emitted
pytest.warns()
Skip conditionally
@pytest.mark.skipif
Document known failure
@pytest.mark.xfail
Run test N times with data
@pytest.mark.parametrize
Shared setup/teardown
@pytest.fixture
Patch objects/env
monkeypatch
Temp files
tmp_path
目标工具
断言相等
assert a == b
断言浮点数相等
pytest.approx
断言异常抛出
pytest.raises()
断言警告触发
pytest.warns()
条件性跳过
@pytest.mark.skipif
记录已知故障
@pytest.mark.xfail
多数据重复运行测试
@pytest.mark.parametrize
共享前置/后置处理
@pytest.fixture
补丁对象/环境
monkeypatch
临时文件
tmp_path

Additional Resources

额外资源

Reference Files

参考文档

  • references/fixtures.md
    — Fixture scopes, autouse, factory pattern, safe teardown patterns
  • references/configuration.md
    — Full
    pyproject.toml
    /
    pytest.ini
    option reference and strict mode details
  • references/fixtures.md
    —— Fixture作用域、自动使用、工厂模式、安全后置处理模式
  • references/configuration.md
    —— 完整的
    pyproject.toml
    /
    pytest.ini
    选项参考及严格模式细节