09.pytest的常用内置插件

Pytest 的常用内置插件详解

1. 什么是 Pytest 插件

1.1 插件的基本概念

插件(Plugin) 是 pytest 框架的核心机制之一,它允许扩展和定制 pytest 的功能。你可以把插件理解为 pytest 的”扩展包”,它们可以:

  • 增强功能:添加新的命令行选项、钩子函数、fixture 等
  • 改变行为:修改测试发现、执行、报告等行为
  • 集成工具:与其他工具(如覆盖率工具、报告生成器等)集成

1.2 插件的分类

Pytest 的插件主要分为两类:

  1. 内置插件(Built-in Plugins)

    • pytest 自带的插件,无需安装即可使用
    • 例如:测试发现插件、断言重写插件、标记插件等
  2. 第三方插件(Third-party Plugins)

    • 需要单独安装的插件
    • 例如:pytest-cov(覆盖率)、pytest-html(HTML报告)等

1.3 为什么需要插件

场景 1:需要生成测试报告

# 没有插件的情况
def test_example():
    assert 1 + 1 == 2

# 运行测试后,只能看到简单的文本输出
# 无法生成美观的 HTML 报告

使用插件后

# 安装 pytest-html 插件
pip install pytest-html

# 运行测试并生成 HTML 报告
pytest --html=report.html

场景 2:需要查看代码覆盖率

# 没有插件的情况
# 无法知道测试覆盖了多少代码

使用插件后

# 安装 pytest-cov 插件
pip install pytest-cov

# 运行测试并查看覆盖率
pytest --cov=myproject --cov-report=html

2. 如何查看和管理插件

2.1 查看已安装的插件

2.1.1 查看所有活跃插件

方法 1:使用 --trace-config 参数

pytest --trace-config

输出示例

============================= test session starts ==============================
platform win32 -- Python 3.9.0, pytest-7.0.0
cachedir: .pytest_cache
rootdir: C:UsersAdministratorDesktoppython自动化测试
plugins: 
  - cov-4.0.0
  - html-3.1.1
  - xdist-3.0.0
active plugins:
  - cacheprovider: /path/to/pytest_cacheprovider.py
  - capture: /path/to/pytest_capture.py
  - ...

方法 2:使用 -p 参数查看特定插件

# 查看某个插件是否已安装
pytest -p pytest_cov

2.1.2 查看插件版本

# 查看 pytest 版本和已安装的插件
pytest --version

# 或者使用 pip 查看
pip list | grep pytest

输出示例

pytest 7.0.0
pytest-cov 4.0.0
pytest-html 3.1.1
pytest-xdist 3.0.0

2.2 安装插件

2.2.1 使用 pip 安装

# 安装单个插件
pip install pytest-html

# 安装多个插件
pip install pytest-html pytest-cov pytest-xdist

# 从 requirements.txt 安装
pip install -r requirements.txt

requirements.txt 示例

pytest>=7.0.0
pytest-html>=3.1.0
pytest-cov>=4.0.0
pytest-xdist>=3.0.0
pytest-timeout>=2.1.0

2.2.2 验证安装

# 运行 pytest --trace-config 查看插件是否已加载
pytest --trace-config | grep html

# 或者直接运行插件提供的功能
pytest --html=test.html

2.3 禁用插件

有时候,你可能需要临时禁用某个插件:

# 禁用某个插件(使用 -p no:插件名)
pytest -p no:html

# 禁用多个插件
pytest -p no:html -p no:cov

# 在 pytest.ini 中永久禁用

pytest.ini 配置示例

[pytest]
# 禁用 html 插件
addopts = -p no:html

2.4 插件配置文件

2.4.1 pytest.ini 配置

[pytest]
# 添加命令行选项
addopts = 
    --html=report.html
    --cov=myproject
    --cov-report=html
    -v

# 指定测试目录
testpaths = tests

# 指定测试文件模式
python_files = test_*.py

# 指定测试类模式
python_classes = Test*

# 指定测试函数模式
python_functions = test_*

2.4.2 setup.cfg 配置

[tool:pytest]
addopts = 
    --html=report.html
    --cov=myproject
    -v
testpaths = tests

2.4.3 pyproject.toml 配置

[tool.pytest.ini_options]
addopts = [
    "--html=report.html",
    "--cov=myproject",
    "-v"
]
testpaths = ["tests"]

3. Pytest 内置核心插件

3.1 测试发现插件(Test Discovery)

3.1.1 什么是测试发现

测试发现插件负责自动找到项目中的测试文件和测试函数。这是 pytest 的核心功能之一。

3.1.2 默认发现规则

文件命名规则

  • test_*.py:以 test_ 开头的 Python 文件
  • *_test.py:以 _test 结尾的 Python 文件

函数命名规则

  • test_*:以 test_ 开头的函数

类命名规则

  • Test*:以 Test 开头的类(不能有 __init__ 方法)

3.1.3 示例

项目结构

project/
├── test_math.py          # 会被发现
├── math_test.py          # 会被发现
├── test_example.py       # 会被发现
├── example.py            # 不会被发现
└── utils.py              # 不会被发现

test_math.py

# 这个文件会被发现
def test_addition():
    assert 1 + 1 == 2

def test_subtraction():
    assert 3 - 1 == 2

# 这个函数不会被当作测试
def helper_function():
    return True

math_test.py

# 这个文件也会被发现
def test_multiplication():
    assert 2 * 3 == 6

运行测试

# 自动发现所有测试
pytest

# 输出:
# test_math.py::test_addition PASSED
# test_math.py::test_subtraction PASSED
# math_test.py::test_multiplication PASSED

3.1.4 自定义发现规则

pytest.ini 配置

[pytest]
# 自定义测试文件模式
python_files = test_*.py check_*.py

# 自定义测试类模式
python_classes = Test* Check*

# 自定义测试函数模式
python_functions = test_* check_*

3.2 断言重写插件(Assertion Rewriting)

3.2.1 什么是断言重写

断言重写是 pytest 的一个强大特性,它会在测试失败时显示详细的断言信息,而不需要你手动添加调试信息。

3.2.2 为什么需要断言重写

标准 assert 的问题

# 标准 assert 在失败时只显示简单的错误
def test_example():
    x = [1, 2, 3]
    y = [1, 2, 4]
    assert x == y  # 失败时只显示:AssertionError

pytest 的断言重写

# pytest 会自动显示详细的比较信息
def test_example():
    x = [1, 2, 3]
    y = [1, 2, 4]
    assert x == y

失败输出

AssertionError: assert [1, 2, 3] == [1, 2, 4]
  At index 2 diff: 3 != 4
  Use -v to get more diff

3.2.3 详细示例

示例 1:列表比较

def test_list_comparison():
    expected = [1, 2, 3, 4, 5]
    actual = [1, 2, 3, 5, 4]
    assert expected == actual

失败输出

AssertionError: assert [1, 2, 3, 4, 5] == [1, 2, 3, 5, 4]
  At index 3 diff: 4 != 5
  At index 4 diff: 5 != 4

示例 2:字典比较

def test_dict_comparison():
    expected = {"name": "张三", "age": 25}
    actual = {"name": "李四", "age": 25}
    assert expected == actual

失败输出

AssertionError: assert {'name': '张三', 'age': 25} == {'name': '李四', 'age': 25}
  Omitting 1 identical items, use -v to show all
  Differing items:
  {'name': '张三'} != {'name': '李四'}

示例 3:字符串比较

def test_string_comparison():
    expected = "Hello World"
    actual = "Hello Python"
    assert expected == actual

失败输出

AssertionError: assert 'Hello World' == 'Hello Python'
  - Hello World
  + Hello Python
  ?       ^^^^

3.2.4 使用 -v 参数获取更多信息

# 使用 -v 参数查看更详细的差异
pytest -v test_example.py

3.3 标记插件(Marking)

3.3.1 什么是标记

标记(Mark)允许你给测试用例添加”标签”,然后可以基于这些标签来筛选和运行特定的测试。

3.3.2 内置标记

1. @pytest.mark.skip:跳过测试

import pytest

@pytest.mark.skip(reason="这个功能还没实现")
def test_new_feature():
    assert False

# 无条件跳过
@pytest.mark.skip
def test_old_feature():
    assert True

运行结果

test_example.py::test_new_feature SKIPPED [1] 这个功能还没实现
test_example.py::test_old_feature SKIPPED [1]

2. @pytest.mark.skipif:条件跳过

import sys
import pytest

# 如果 Python 版本小于 3.8,跳过测试
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要 Python 3.8+")
def test_new_syntax():
    # 使用 Python 3.8+ 的新特性
    pass

# 如果操作系统是 Windows,跳过测试
@pytest.mark.skipif(sys.platform == "win32", reason="不支持 Windows")
def test_unix_only():
    assert True

3. @pytest.mark.xfail:预期失败

import pytest

# 标记为预期失败(已知 bug)
@pytest.mark.xfail(reason="已知 bug,待修复")
def test_buggy_feature():
    assert False  # 这个测试会失败,但这是预期的

# 如果测试通过了,会显示 XPASS
@pytest.mark.xfail
def test_maybe_fixed():
    assert True  # 如果这个通过了,会显示 XPASS

运行结果

test_example.py::test_buggy_feature XFAIL [1] 已知 bug,待修复
test_example.py::test_maybe_fixed XPASS [1]

3.3.3 自定义标记

定义标记

pytest.ini 配置

[pytest]
markers =
    slow: 标记为慢速测试
    integration: 标记为集成测试
    smoke: 标记为冒烟测试
    regression: 标记为回归测试

使用标记

import pytest

@pytest.mark.slow
def test_slow_operation():
    import time
    time.sleep(5)
    assert True

@pytest.mark.integration
def test_integration():
    # 集成测试代码
    assert True

@pytest.mark.smoke
def test_smoke():
    # 冒烟测试代码
    assert True

# 可以同时使用多个标记
@pytest.mark.slow
@pytest.mark.integration
def test_slow_integration():
    assert True

运行特定标记的测试

# 只运行标记为 slow 的测试
pytest -m slow

# 只运行标记为 integration 的测试
pytest -m integration

# 运行标记为 smoke 或 regression 的测试
pytest -m "smoke or regression"

# 运行标记为 slow 且 integration 的测试
pytest -m "slow and integration"

# 运行除了 slow 之外的所有测试
pytest -m "not slow"

3.4 参数化插件(Parametrization)

3.4.1 什么是参数化

参数化允许你使用不同的输入数据运行同一个测试函数,避免编写重复的测试代码。

3.4.2 基本用法

不使用参数化(重复代码)

def test_addition_1():
    assert 1 + 1 == 2

def test_addition_2():
    assert 2 + 2 == 4

def test_addition_3():
    assert 3 + 3 == 6

使用参数化(简洁高效)

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (1, 1, 2),
    (2, 2, 4),
    (3, 3, 6),
])
def test_addition(a, b, expected):
    assert a + b == expected

运行结果

test_example.py::test_addition[1-1-2] PASSED
test_example.py::test_addition[2-2-4] PASSED
test_example.py::test_addition[3-3-6] PASSED

3.4.3 详细示例

示例 1:测试字符串方法

import pytest

@pytest.mark.parametrize("input_str, expected", [
    ("hello", "HELLO"),
    ("world", "WORLD"),
    ("pytest", "PYTEST"),
])
def test_upper(input_str, expected):
    assert input_str.upper() == expected

示例 2:测试数学运算

import pytest

@pytest.mark.parametrize("x, y, operation, expected", [
    (2, 3, "add", 5),
    (5, 2, "subtract", 3),
    (3, 4, "multiply", 12),
    (10, 2, "divide", 5),
])
def test_calculator(x, y, operation, expected):
    if operation == "add":
        result = x + y
    elif operation == "subtract":
        result = x - y
    elif operation == "multiply":
        result = x * y
    elif operation == "divide":
        result = x / y

    assert result == expected

示例 3:组合参数化

import pytest

# 第一个参数化
@pytest.mark.parametrize("x", [1, 2, 3])
# 第二个参数化
@pytest.mark.parametrize("y", [10, 20])
def test_combination(x, y):
    # 会运行 3 * 2 = 6 次测试
    assert (x + y) > 0

运行结果

test_example.py::test_combination[1-10] PASSED
test_example.py::test_combination[1-20] PASSED
test_example.py::test_combination[2-10] PASSED
test_example.py::test_combination[2-20] PASSED
test_example.py::test_combination[3-10] PASSED
test_example.py::test_combination[3-20] PASSED

3.4.4 参数化与标记结合

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (1, 1, 2),
    (2, 2, 4),
    pytest.param(3, 3, 6, marks=pytest.mark.slow),
    pytest.param(4, 4, 8, marks=[pytest.mark.slow, pytest.mark.integration]),
])
def test_with_marks(a, b, expected):
    assert a + b == expected

3.5 Fixture 插件

3.5.1 什么是 Fixture

Fixture 是 pytest 中用于提供测试数据和测试环境的功能。关于 Fixture 的详细内容,请参考 07.pytest的fixture.md 文档。

3.5.2 内置 Fixture

1. tmp_path:临时目录

def test_create_file(tmp_path):
    # tmp_path 是一个 Path 对象,指向临时目录
    file_path = tmp_path / "test.txt"
    file_path.write_text("Hello, pytest!")

    assert file_path.read_text() == "Hello, pytest!"
    assert file_path.exists()

2. tmpdir:临时目录(旧版本,推荐使用 tmp_path)

def test_create_file_old(tmpdir):
    # tmpdir 是一个 py.path.local 对象
    file_path = tmpdir.join("test.txt")
    file_path.write("Hello, pytest!")

    assert file_path.read() == "Hello, pytest!"

3. capsys:捕获标准输出

def test_print_output(capsys):
    print("Hello, World!")
    captured = capsys.readouterr()
    assert captured.out == "Hello, World!n"

4. capfd:捕获文件描述符输出

def test_stderr_output(capfd):
    import sys
    sys.stderr.write("Error message")
    captured = capfd.readouterr()
    assert captured.err == "Error message"

5. monkeypatch:临时修改环境

def test_env_variable(monkeypatch):
    # 设置环境变量
    monkeypatch.setenv("MY_VAR", "test_value")
    import os
    assert os.environ["MY_VAR"] == "test_value"

    # 测试结束后,环境变量会自动恢复

def test_sys_path(monkeypatch):
    import sys
    original_path = sys.path.copy()

    # 修改 sys.path
    monkeypatch.syspath_prepend("/custom/path")

    # 测试结束后,sys.path 会自动恢复

6. request:访问测试请求信息

def test_request_info(request):
    # 获取测试函数名
    print(f"测试函数名: {request.function.__name__}")

    # 获取测试文件路径
    print(f"测试文件: {request.fspath}")

    # 获取标记
    print(f"标记: {request.node.get_closest_marker('slow')}")

4. 常用第三方插件

4.1 pytest-html:生成 HTML 报告

4.1.1 安装

pip install pytest-html

4.1.2 基本用法

# 生成 HTML 报告
pytest --html=report.html

# 生成 HTML 报告并包含 CSS 样式(自包含)
pytest --html=report.html --self-contained-html

4.1.3 详细示例

测试文件:test_example.py

def test_addition():
    assert 1 + 1 == 2

def test_subtraction():
    assert 3 - 1 == 2

def test_failure():
    assert 1 == 2  # 这个会失败

运行测试

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

生成的 HTML 报告包含

  • 测试摘要(通过、失败、跳过数量)
  • 详细的测试结果
  • 失败测试的错误信息
  • 测试执行时间
  • 可以通过浏览器打开查看

4.1.4 配置选项

pytest.ini 配置

[pytest]
addopts = --html=report.html --self-contained-html

自定义报告标题

pytest --html=report.html --html-title="我的测试报告"

4.2 pytest-cov:代码覆盖率

4.2.1 安装

pip install pytest-cov

4.2.2 基本用法

# 测量覆盖率
pytest --cov=myproject

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

# 生成终端报告
pytest --cov=myproject --cov-report=term

# 生成 XML 报告(用于 CI/CD)
pytest --cov=myproject --cov-report=xml

4.2.3 详细示例

项目结构

myproject/
├── myproject/
│   ├── __init__.py
│   ├── math.py
│   └── string.py
└── tests/
    ├── test_math.py
    └── test_string.py

myproject/math.py

def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

def multiply(a, b):
    return a * b

def divide(a, b):
    if b == 0:
        raise ValueError("除数不能为0")
    return a / b

tests/test_math.py

from myproject.math import add, subtract

def test_add():
    assert add(1, 2) == 3

def test_subtract():
    assert subtract(5, 2) == 3

运行覆盖率测试

pytest --cov=myproject --cov-report=html --cov-report=term

输出示例

---------- coverage: platform win32, python 3.9.0 -----------
Name                 Stmts   Miss  Cover
----------------------------------------
myproject/__init__.py       0      0   100%
myproject/math.py           8      4    50%
myproject/string.py         0      0   100%
----------------------------------------
TOTAL                       8      4    50%

HTML 报告会显示

  • 哪些行被测试覆盖(绿色)
  • 哪些行没有被覆盖(红色)
  • 覆盖率百分比

4.2.4 配置选项

pytest.ini 配置

[pytest]
addopts = 
    --cov=myproject
    --cov-report=html
    --cov-report=term
    --cov-branch  # 包含分支覆盖率
    --cov-fail-under=80  # 覆盖率低于 80% 时失败

.coveragerc 配置文件

[run]
source = myproject
omit = 
    */tests/*
    */test_*.py

[report]
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError

4.3 pytest-xdist:并行执行测试

4.3.1 安装

pip install pytest-xdist

4.3.2 基本用法

# 使用所有 CPU 核心并行运行
pytest -n auto

# 指定使用 4 个进程
pytest -n 4

# 使用所有 CPU 核心(等同于 auto)
pytest -n auto

4.3.3 详细示例

测试文件:test_slow.py

import time

def test_slow_1():
    time.sleep(1)
    assert True

def test_slow_2():
    time.sleep(1)
    assert True

def test_slow_3():
    time.sleep(1)
    assert True

def test_slow_4():
    time.sleep(1)
    assert True

串行运行(不使用 xdist)

pytest test_slow.py -v
# 执行时间:约 4 秒(1秒 * 4个测试)

并行运行(使用 xdist)

pytest test_slow.py -n 4 -v
# 执行时间:约 1 秒(4个测试同时运行)

4.3.4 高级用法

1. 按测试文件分组并行

# 每个测试文件在一个进程中运行
pytest --dist=loadfile -n 4

2. 按测试类分组并行

# 每个测试类在一个进程中运行
pytest --dist=loadscope -n 4

3. 循环失败模式

# 只重新运行失败的测试
pytest --looponfail -n 4

4.3.5 注意事项

  • Fixture 作用域:并行运行时,sessionmodule 作用域的 fixture 会在每个进程中独立创建
  • 共享资源:避免多个进程同时访问同一个文件或数据库
  • 随机性:测试执行顺序可能不同

4.4 pytest-timeout:测试超时控制

4.4.1 安装

pip install pytest-timeout

4.4.2 基本用法

# 设置全局超时时间为 10 秒
pytest --timeout=10

# 设置超时时间为 5 秒,使用信号方式(推荐)
pytest --timeout=5 --timeout-method=thread

4.4.3 详细示例

示例 1:全局超时

import time

def test_slow_operation():
    time.sleep(15)  # 如果超时时间设置为 10 秒,这个测试会失败
    assert True

运行

pytest --timeout=10 test_example.py

输出

test_example.py::test_slow_operation TIMEOUT [10.00s]

示例 2:单个测试超时

import pytest
import time

@pytest.mark.timeout(5)  # 这个测试最多运行 5 秒
def test_with_timeout():
    time.sleep(10)  # 会超时
    assert True

@pytest.mark.timeout(10)  # 这个测试最多运行 10 秒
def test_with_longer_timeout():
    time.sleep(5)  # 不会超时
    assert True

示例 3:使用函数装饰器

import pytest
import time

@pytest.mark.timeout(3, method='thread')
def test_thread_timeout():
    time.sleep(5)
    assert True

4.4.4 配置选项

pytest.ini 配置

[pytest]
addopts = --timeout=10 --timeout-method=thread

超时方法

  • thread:使用线程(推荐,跨平台)
  • signal:使用信号(仅 Unix 系统)

4.5 pytest-rerunfailures:失败重试

4.5.1 安装

pip install pytest-rerunfailures

4.5.2 基本用法

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

# 失败后重试 3 次,每次重试间隔 2 秒
pytest --reruns 3 --reruns-delay 2

4.5.3 详细示例

示例 1:全局重试

import random

def test_flaky_test():
    # 这个测试有时会失败(模拟不稳定的测试)
    result = random.choice([True, False])
    assert result, "随机失败"

运行

pytest --reruns 3 test_example.py -v

输出

test_example.py::test_flaky_test RERUN
test_example.py::test_flaky_test RERUN
test_example.py::test_flaky_test PASSED

示例 2:单个测试重试

import pytest
import random

@pytest.mark.flaky(reruns=5, reruns_delay=1)
def test_specific_retry():
    result = random.choice([True, False])
    assert result

示例 3:条件重试

import pytest

@pytest.mark.flaky(reruns=3, condition=True)
def test_conditional_retry():
    # 只有在 condition=True 时才会重试
    assert False

4.5.4 配置选项

pytest.ini 配置

[pytest]
addopts = --reruns 3 --reruns-delay 2

4.6 pytest-mock:Mock 功能增强

4.6.1 安装

pip install pytest-mock

4.6.2 基本用法

pytest-mock 提供了一个 mocker fixture,它是对 unittest.mock 的封装,使用更方便。

4.6.3 详细示例

示例 1:Mock 函数

def test_mock_function(mocker):
    # Mock 一个函数
    mock_func = mocker.patch('module.function')
    mock_func.return_value = 42

    # 调用被 Mock 的函数
    result = module.function()
    assert result == 42
    mock_func.assert_called_once()

示例 2:Mock 对象方法

class Calculator:
    def add(self, a, b):
        return a + b

def test_mock_method(mocker):
    calc = Calculator()
    # Mock add 方法
    mocker.patch.object(calc, 'add', return_value=100)

    result = calc.add(1, 2)
    assert result == 100

示例 3:Mock 环境变量

def test_mock_env(mocker):
    mocker.patch.dict('os.environ', {'MY_VAR': 'test_value'})
    import os
    assert os.environ['MY_VAR'] == 'test_value'

示例 4:Mock HTTP 请求

import requests

def test_mock_http(mocker):
    # Mock requests.get
    mock_get = mocker.patch('requests.get')
    mock_get.return_value.json.return_value = {'status': 'ok'}
    mock_get.return_value.status_code = 200

    response = requests.get('http://example.com/api')
    assert response.json() == {'status': 'ok'}
    assert response.status_code == 200

4.7 pytest-asyncio:异步测试支持

4.7.1 安装

pip install pytest-asyncio

4.7.2 基本用法

import pytest
import asyncio

@pytest.mark.asyncio
async def test_async_function():
    await asyncio.sleep(0.1)
    assert True

@pytest.mark.asyncio
async def test_async_http():
    import aiohttp
    async with aiohttp.ClientSession() as session:
        async with session.get('http://httpbin.org/get') as response:
            assert response.status == 200

4.7.3 详细示例

示例 1:测试异步函数

import pytest
import asyncio

async def fetch_data():
    await asyncio.sleep(0.1)
    return {"data": "test"}

@pytest.mark.asyncio
async def test_fetch_data():
    result = await fetch_data()
    assert result == {"data": "test"}

示例 2:异步 Fixture

import pytest

@pytest.fixture
async def async_fixture():
    # 异步前置操作
    data = await setup_async_data()
    yield data
    # 异步后置清理
    await cleanup_async_data(data)

@pytest.mark.asyncio
async def test_with_async_fixture(async_fixture):
    assert async_fixture is not None

示例 3:配置异步模式

pytest.ini 配置

[pytest]
asyncio_mode = auto

4.8 pytest-json-report:JSON 报告

4.8.1 安装

pip install pytest-json-report

4.8.2 基本用法

# 生成 JSON 报告
pytest --json-report --json-report-file=report.json

4.8.3 详细示例

运行测试

pytest --json-report --json-report-file=report.json -v

生成的 JSON 报告示例

{
  "created": 1234567890.123,
  "duration": 1.234,
  "exitcode": 0,
  "root": "/path/to/project",
  "environment": {
    "Python": "3.9.0",
    "Platform": "Windows-10"
  },
  "summary": {
    "passed": 10,
    "failed": 2,
    "skipped": 1,
    "total": 13
  },
  "tests": [
    {
      "nodeid": "test_example.py::test_addition",
      "outcome": "passed",
      "duration": 0.001,
      "setup": {
        "duration": 0.0005,
        "outcome": "passed"
      },
      "call": {
        "duration": 0.001,
        "outcome": "passed"
      }
    }
  ]
}

4.9 pytest-sugar:美化输出

4.9.1 安装

pip install pytest-sugar

4.9.2 功能

pytest-sugar 会自动美化 pytest 的输出,显示:

  • 彩色输出
  • 进度条
  • 即时显示失败信息(不需要等待所有测试完成)

4.9.3 使用

安装后自动生效,无需额外配置:

pytest -v

输出对比

不使用 pytest-sugar

test_example.py ...                                          [100%]

使用 pytest-sugar

test_example.py ✓✓✓                                          [100%]

4.10 pytest-benchmark:性能测试

4.10.1 安装

pip install pytest-benchmark

4.10.2 基本用法

def test_performance(benchmark):
    result = benchmark(lambda: sum(range(1000)))
    assert result == 499500

4.10.3 详细示例

示例 1:基准测试函数

def test_list_comprehension(benchmark):
    result = benchmark(lambda: [x * 2 for x in range(1000)])
    assert len(result) == 1000

def test_map_function(benchmark):
    result = benchmark(lambda: list(map(lambda x: x * 2, range(1000))))
    assert len(result) == 1000

运行

pytest test_benchmark.py --benchmark-only

输出

-------------------------------- benchmark: 2 tests --------------------------------
Name (time in us)        Min       Max      Mean    StdDev
----------------------------------------------------------------------------------------
test_list_comprehension  45.23    67.89    52.34    3.21
test_map_function        78.45   102.34    89.12    5.67
----------------------------------------------------------------------------------------

示例 2:比较不同实现

def test_algorithm_v1(benchmark):
    def algorithm():
        return sum(range(1000))
    benchmark(algorithm)

def test_algorithm_v2(benchmark):
    def algorithm():
        total = 0
        for i in range(1000):
            total += i
        return total
    benchmark(algorithm)

5. 插件组合使用

5.1 常用组合配置

在实际项目中,通常会同时使用多个插件。以下是一些常见的组合:

5.1.1 基础测试配置

pytest.ini

[pytest]
addopts = 
    -v
    --html=report.html
    --self-contained-html
    --cov=myproject
    --cov-report=html
    --cov-report=term
    --cov-fail-under=80
markers =
    slow: 慢速测试
    integration: 集成测试
    smoke: 冒烟测试

5.1.2 CI/CD 配置

pytest.ini

[pytest]
addopts = 
    -v
    --cov=myproject
    --cov-report=xml
    --cov-report=term
    --junitxml=junit.xml
    --html=report.html
    --self-contained-html
    -n auto
    --reruns 2
    --reruns-delay 1

5.1.3 开发环境配置

pytest.ini

[pytest]
addopts = 
    -v
    --tb=short
    --cov=myproject
    --cov-report=term-missing
    -n auto
    --looponfail
markers =
    slow: 慢速测试(跳过)

运行慢速测试

# 默认跳过慢速测试
pytest

# 显式运行慢速测试
pytest -m slow

5.2 完整示例项目

项目结构

myproject/
├── myproject/
│   ├── __init__.py
│   ├── calculator.py
│   └── utils.py
├── tests/
│   ├── __init__.py
│   ├── test_calculator.py
│   └── test_utils.py
├── pytest.ini
├── requirements.txt
└── README.md

requirements.txt

pytest>=7.0.0
pytest-html>=3.1.0
pytest-cov>=4.0.0
pytest-xdist>=3.0.0
pytest-timeout>=2.1.0
pytest-rerunfailures>=11.0.0
pytest-sugar>=0.9.6

pytest.ini

[pytest]
addopts = 
    -v
    --html=reports/report.html
    --self-contained-html
    --cov=myproject
    --cov-report=html:reports/coverage
    --cov-report=term
    --cov-branch
    --cov-fail-under=80
    -n auto
    --reruns 2
    --reruns-delay 1
    --timeout=30
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
    slow: 标记为慢速测试
    integration: 标记为集成测试
    smoke: 标记为冒烟测试
    unit: 标记为单元测试

myproject/calculator.py

class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("除数不能为0")
        return a / b

tests/test_calculator.py

import pytest
from myproject.calculator import Calculator

@pytest.fixture
def calc():
    return Calculator()

@pytest.mark.unit
@pytest.mark.parametrize("a, b, expected", [
    (1, 2, 3),
    (5, 3, 8),
    (10, 20, 30),
])
def test_add(calc, a, b, expected):
    assert calc.add(a, b) == expected

@pytest.mark.unit
def test_subtract(calc):
    assert calc.subtract(5, 2) == 3

@pytest.mark.unit
def test_multiply(calc):
    assert calc.multiply(3, 4) == 12

@pytest.mark.unit
def test_divide(calc):
    assert calc.divide(10, 2) == 5

@pytest.mark.unit
def test_divide_by_zero(calc):
    with pytest.raises(ValueError):
        calc.divide(10, 0)

@pytest.mark.slow
def test_slow_operation(calc):
    import time
    time.sleep(2)
    assert calc.add(1, 1) == 2

运行测试

# 运行所有测试
pytest

# 只运行单元测试
pytest -m unit

# 跳过慢速测试
pytest -m "not slow"

# 生成报告
pytest --html=reports/report.html --cov=myproject

发表评论