pytest_自动化测试3

4. pytest测试执行控制与组织

4.1 命令行参数详解

pytest提供了丰富的命令行参数,用于控制测试执行行为。

常用命令行参数
python 复制代码
# test_command_line_args.py
import pytest
import time

# 基础命令行参数测试
def test_basic_functionality():
    """
    基础功能测试
    命令:pytest test_command_line_args.py
    """
    assert True

def test_verbose_output():
    """
    详细输出测试
    命令:pytest -v test_command_line_args.py
    -v 或 --verbose:显示详细的测试输出
    """
    assert True

def test_print_output():
    """
    打印输出测试
    命令:pytest -s test_command_line_args.py
    -s 或 --capture=no:显示print输出
    """
    print("这条消息会被显示")
    assert True

@pytest.mark.slow
def test_slow_test():
    """
    慢速测试
    命令:pytest -m "not slow" test_command_line_args.py
    -m 或 --mark:按标记过滤测试
    """
    time.sleep(0.1)
    assert True

def test_keyword_filter():
    """
    关键字过滤测试
    命令:pytest -k "keyword" test_command_line_args.py
    -k 或 --keyword:按关键字过滤测试
    """
    assert True

def test_stop_on_failure():
    """
    失败停止测试
    命令:pytest -x test_command_line_args.py
    -x 或 --exitfirst:第一个失败后停止
    """
    assert True

def test_max_failures():
    """
    最大失败次数测试
    命令:pytest --maxfail=2 test_command_line_args.py
    --maxfail:达到指定失败次数后停止
    """
    assert True

4.2 测试选择与过滤方法

pytest提供了多种方式来选择和过滤测试。

测试选择与过滤示例
python 复制代码
# test_selection_filtering.py
import pytest

# 按文件名选择
def test_file_selection():
    """
    文件选择测试
    命令:pytest test_selection_filtering.py
    只运行指定文件的测试
    """
    assert True

# 按目录选择
def test_directory_selection():
    """
    目录选择测试
    命令:pytest tests/
    只运行指定目录的测试
    """
    assert True

# 按关键字过滤
def test_keyword_filtering():
    """
    关键字过滤测试
    命令:pytest -k "keyword"
    只运行包含关键字的测试
    """
    assert True

# 按标记过滤
@pytest.mark.unit
def test_mark_filtering_unit():
    """
    标记过滤测试 - 单元测试
    命令:pytest -m unit
    只运行带有unit标记的测试
    """
    assert True

@pytest.mark.integration
def test_mark_filtering_integration():
    """
    标记过滤测试 - 集成测试
    命令:pytest -m integration
    只运行带有integration标记的测试
    """
    assert True

@pytest.mark.slow
def test_mark_filtering_slow():
    """
    标记过滤测试 - 慢速测试
    命令:pytest -m "not slow"
    跳过带有slow标记的测试
    """
    assert True

# 按节点ID选择
def test_node_id_selection():
    """
    节点ID选择测试
    命令:pytest test_selection_filtering.py::test_node_id_selection
    只运行指定节点的测试
    """
    assert True

# 组合过滤
@pytest.mark.unit
@pytest.mark.fast
def test_combined_filtering():
    """
    组合过滤测试
    命令:pytest -m "unit and fast"
    只运行同时带有unit和fast标记的测试
    """
    assert True

4.3 测试执行顺序控制

pytest允许控制测试的执行顺序。

测试执行顺序控制示例
python 复制代码
# test_execution_order.py
import pytest

# 默认执行顺序
def test_default_order_1():
    """
    默认执行顺序测试1
    pytest默认按文件名、类名、函数名的字母顺序执行
    """
    print("执行test_default_order_1")
    assert True

def test_default_order_2():
    """
    默认执行顺序测试2
    """
    print("执行test_default_order_2")
    assert True

# 使用pytest-ordering插件控制顺序
"""
安装:pip install pytest-ordering

使用方法:
@pytest.mark.run(order=1)
def test_first():
    pass

@pytest.mark.run(order=2)
def test_second():
    pass
"""

# 使用依赖关系控制顺序
@pytest.mark.dependency()
def test_dependency_first():
    """
    依赖关系测试 - 第一个测试
    需要安装:pip install pytest-dependency
    """
    print("执行test_dependency_first")
    assert True

@pytest.mark.dependency(depends=["test_dependency_first"])
def test_dependency_second():
    """
    依赖关系测试 - 第二个测试
    依赖于test_dependency_first
    """
    print("执行test_dependency_second")
    assert True

# 使用session fixture控制顺序
@pytest.fixture(scope="session", autouse=True)
def execution_order_control():
    """
    执行顺序控制fixture
    可以在fixture中控制执行顺序
    """
    print("开始测试会话")
    yield
    print("结束测试会话")

4.4 测试重试机制(pytest-rerunfailures参数详解)

pytest-rerunfailures插件提供了测试重试功能。

测试重试机制示例
python 复制代码
# test_rerunfailures.py
import pytest
import random

# 基础重试测试
@pytest.mark.flaky
def test_flaky_with_retry():
    """
    不稳定测试,带有重试标记
    命令:pytest --reruns 3 test_rerunfailures.py
    --reruns:重试次数
    """
    # 模拟不稳定的测试
    if random.random() < 0.5:
        raise AssertionError("随机失败")
    assert True

# 带延迟的重试
@pytest.mark.flaky
def test_retry_with_delay():
    """
    带延迟的重试测试
    命令:pytest --reruns 3 --reruns-delay 1 test_rerunfailures.py
    --reruns-delay:重试之间的延迟(秒)
    """
    if random.random() < 0.5:
        raise AssertionError("随机失败")
    assert True

# 条件重试
@pytest.mark.flaky(reruns=2, reruns_delay=0.5)
def test_conditional_retry():
    """
    条件重试测试
    使用标记参数控制重试行为
    """
    if random.random() < 0.5:
        raise AssertionError("随机失败")
    assert True

# 排除重试
@pytest.mark.flaky(reruns=3)
def test_excluded_from_retry():
    """
    排除重试测试
    命令:pytest --reruns 3 --rerun-exclude "excluded" test_rerunfailures.py
    --rerun-exclude:排除特定标记的重试
    """
    raise AssertionError("这个测试不应该重试")

4.5 测试超时处理(pytest-timeout参数详解)

pytest-timeout插件提供了测试超时功能。

测试超时处理示例
python 复制代码
# test_timeout.py
import pytest
import time

# 基础超时测试
def test_basic_timeout():
    """
    基础超时测试
    命令:pytest --timeout=2 test_timeout.py
    --timeout:全局超时时间(秒)
    """
    time.sleep(1)
    assert True

# 标记超时
@pytest.mark.timeout(1)
def test_marked_timeout():
    """
    标记超时测试
    使用标记为特定测试设置超时
    """
    time.sleep(0.5)
    assert True

# 超时失败测试
@pytest.mark.timeout(0.5)
def test_timeout_failure():
    """
    超时失败测试
    这个测试会因为超时而失败
    """
    time.sleep(1)
    assert True  # 这行不会执行

# 超时方法
@pytest.mark.timeout(method="thread")
def test_thread_timeout():
    """
    线程超时测试
    使用线程方法实现超时
    """
    time.sleep(0.5)
    assert True

@pytest.mark.timeout(method="signal")
def test_signal_timeout():
    """
    信号超时测试
    使用信号方法实现超时(仅限Unix)
    """
    time.sleep(0.5)
    assert True

4.6 并发执行配置(pytest-xdist参数详解)

pytest-xdist插件提供了并发执行测试的功能。

并发执行配置示例
python 复制代码
# test_xdist.py
import pytest
import time

# 基础并发测试
def test_concurrent_1():
    """
    并发测试1
    命令:pytest -n 4 test_xdist.py
    -n 或 --numprocesses:并发进程数
    """
    time.sleep(0.5)
    assert True

def test_concurrent_2():
    """
    并发测试2
    """
    time.sleep(0.5)
    assert True

def test_concurrent_3():
    """
    并发测试3
    """
    time.sleep(0.5)
    assert True

def test_concurrent_4():
    """
    并发测试4
    """
    time.sleep(0.5)
    assert True

# 自动检测CPU核心数
"""
命令:pytest -n auto test_xdist.py
自动检测CPU核心数并使用相应的进程数
"""

# 分布式执行
"""
命令:pytest --dist=each --tx 2*popen//python=python3 test_xdist.py
--dist:分发策略(each, loadscope)
--tx:执行环境配置
"""

# 锁定测试顺序
"""
命令:pytest -n 4 --dist=loadscope test_xdist.py
--dist=loadscope:按模块/类分发测试,保持测试顺序
"""
性能调优实战案例

下面通过一个实际的性能调优案例,展示如何优化pytest测试套件的执行速度。

python 复制代码
# performance_optimization/src/data_processor.py
"""
数据处理器 - 需要测试的模块
"""
import time
from typing import List, Dict

class DataProcessor:
    """
    数据处理器类
    """
    
    def __init__(self):
        """
        初始化数据处理器
        """
        self.data: List[Dict] = []
        self.cache: Dict = {}
    
    def load_data(self, count: int = 1000) -> List[Dict]:
        """
        加载数据(模拟慢速操作)
        """
        time.sleep(0.1)  # 模拟数据库查询
        self.data = [{"id": i, "value": f"data_{i}"} for i in range(count)]
        return self.data
    
    def process_data(self, data: List[Dict]) -> List[Dict]:
        """
        处理数据
        """
        return [{"id": item["id"], "processed": True} for item in data]
    
    def save_data(self, data: List[Dict]) -> bool:
        """
        保存数据(模拟慢速操作)
        """
        time.sleep(0.1)  # 模拟数据库写入
        return True
    
    def get_cached_result(self, key: str) -> Dict:
        """
        获取缓存结果
        """
        return self.cache.get(key, {})
    
    def set_cached_result(self, key: str, value: Dict):
        """
        设置缓存结果
        """
        self.cache[key] = value

# performance_optimization/tests/test_data_processor_slow.py
"""
未优化的测试套件 - 执行速度较慢
"""
import pytest
from src.data_processor import DataProcessor

class TestDataProcessorSlow:
    """
    数据处理器测试 - 未优化版本
    """
    
    def test_load_data(self):
        """
        测试数据加载
        每次测试都创建新的处理器实例
        """
        processor = DataProcessor()
        data = processor.load_data(100)
        assert len(data) == 100
    
    def test_process_data(self):
        """
        测试数据处理
        每次测试都创建新的处理器实例
        """
        processor = DataProcessor()
        data = processor.load_data(100)
        processed = processor.process_data(data)
        assert len(processed) == 100
    
    def test_save_data(self):
        """
        测试数据保存
        每次测试都创建新的处理器实例
        """
        processor = DataProcessor()
        data = processor.load_data(100)
        result = processor.save_data(data)
        assert result is True
    
    def test_full_workflow(self):
        """
        测试完整工作流
        """
        processor = DataProcessor()
        data = processor.load_data(100)
        processed = processor.process_data(data)
        result = processor.save_data(processed)
        assert result is True
    
    def test_cache_operations(self):
        """
        测试缓存操作
        """
        processor = DataProcessor()
        processor.set_cached_result("key1", {"value": "test"})
        cached = processor.get_cached_result("key1")
        assert cached["value"] == "test"

# performance_optimization/tests/test_data_processor_optimized.py
"""
优化后的测试套件 - 执行速度显著提升
"""
import pytest
from src.data_processor import DataProcessor

# 优化1:使用session级别的fixture减少初始化开销
@pytest.fixture(scope="session")
def shared_processor():
    """
    共享的处理器实例
    session级别,整个测试会话只创建一次
    """
    return DataProcessor()

# 优化2:使用module级别的fixture
@pytest.fixture(scope="module")
def module_processor():
    """
    模块级别的处理器实例
    每个测试模块创建一次
    """
    return DataProcessor()

# 优化3:使用缓存fixture
@pytest.fixture
def cached_data(cache):
    """
    缓存的数据fixture
    避免重复计算
    """
    cache_key = "test_data_100"
    if cache_key in cache:
        return cache[cache_key]
    
    processor = DataProcessor()
    data = processor.load_data(100)
    cache[cache_key] = data
    return data

class TestDataProcessorOptimized:
    """
    数据处理器测试 - 优化版本
    """
    
    def test_load_data_with_cache(self, cached_data):
        """
        测试数据加载(使用缓存)
        使用缓存的数据,避免重复加载
        """
        assert len(cached_data) == 100
    
    def test_process_data_with_cache(self, cached_data):
        """
        测试数据处理(使用缓存)
        """
        processor = DataProcessor()
        processed = processor.process_data(cached_data)
        assert len(processed) == 100
    
    def test_save_data_with_cache(self, cached_data):
        """
        测试数据保存(使用缓存)
        """
        processor = DataProcessor()
        result = processor.save_data(cached_data)
        assert result is True
    
    def test_full_workflow_optimized(self, cached_data):
        """
        测试完整工作流(优化版)
        使用缓存数据,减少重复操作
        """
        processor = DataProcessor()
        processed = processor.process_data(cached_data)
        result = processor.save_data(processed)
        assert result is True

# 优化4:使用参数化测试减少重复代码
@pytest.mark.parametrize("data_count,expected_length", [
    (10, 10),
    (50, 50),
    (100, 100),
    (500, 500),
])
def test_load_data_various_sizes(data_count, expected_length):
    """
    测试加载不同大小的数据
    使用参数化测试,避免编写多个相似的测试
    """
    processor = DataProcessor()
    data = processor.load_data(data_count)
    assert len(data) == expected_length

# 优化5:使用mock避免慢速操作
def test_process_data_with_mock(mocker):
    """
    测试数据处理(使用mock)
    mock慢速的数据库操作
    """
    processor = DataProcessor()
    
    # mock load_data方法,避免实际的数据库查询
    mocker.patch.object(processor, 'load_data', 
                     return_value=[{"id": i, "value": f"data_{i}"} for i in range(100)])
    
    data = processor.load_data(100)
    processed = processor.process_data(data)
    assert len(processed) == 100

# 优化6:使用标记组织测试
@pytest.mark.unit
@pytest.mark.fast
def test_cache_operations_fast(self):
    """
    测试缓存操作(快速测试)
    不涉及数据库操作,执行速度快
    """
    processor = DataProcessor()
    processor.set_cached_result("key1", {"value": "test"})
    cached = processor.get_cached_result("key1")
    assert cached["value"] == "test"

@pytest.mark.integration
@pytest.mark.slow
def test_full_database_workflow(self):
    """
    测试完整数据库工作流(慢速测试)
    涉及数据库操作,执行速度慢
    """
    processor = DataProcessor()
    data = processor.load_data(1000)
    processed = processor.process_data(data)
    result = processor.save_data(processed)
    assert result is True

# performance_optimization/tests/conftest.py
"""
性能优化的配置文件
"""
import pytest

def pytest_configure(config):
    """
    配置pytest
    """
    config.addinivalue_line("markers", "unit: 单元测试")
    config.addinivalue_line("markers", "integration: 集成测试")
    config.addinivalue_line("markers", "fast: 快速测试")
    config.addinivalue_line("markers", "slow: 慢速测试")

# 性能测试fixture
@pytest.fixture
def benchmark_data():
    """
    基准测试数据
    """
    return {
        "small": 10,
        "medium": 100,
        "large": 1000,
        "xlarge": 10000
    }

# performance_optimization/pytest.ini
"""
pytest配置文件 - 性能优化配置
"""
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# 性能优化配置
addopts = 
    -v
    --strict-markers
    --tb=short
    --maxfail=5
    --dist=loadscope

# 标记定义
markers =
    unit: 单元测试(快速)
    integration: 集成测试(慢速)
    fast: 快速测试
    slow: 慢速测试

# 并发配置
# 使用CPU核心数的75%作为并发数
# numprocesses = auto

# 性能优化建议
"""
1. 使用session级别的fixture减少初始化开销
2. 使用缓存避免重复计算
3. 使用mock隔离慢速操作
4. 使用参数化测试减少重复代码
5. 使用标记组织测试,选择性运行
6. 使用并发执行加速测试
7. 使用--maxfail避免浪费时间在失败的测试上
8. 使用--dist=loadscope保持测试顺序
"""

# 性能对比测试
"""
未优化版本的执行时间:
- test_data_processor_slow.py: 4个测试 × 0.2秒 = 0.8秒

优化版本的执行时间:
- test_data_processor_optimized.py: 4个测试 × 0.05秒 = 0.2秒

性能提升:4倍

使用并发执行:
- pytest -n 4: 0.2秒 / 4 = 0.05秒

总体性能提升:16倍
"""

# 性能监控命令
"""
# 运行性能测试
pytest tests/ -v --durations=10

# 运行最慢的10个测试
pytest tests/ --durations=10

# 生成性能报告
pytest tests/ -v --durations=0 > performance_report.txt

# 使用pytest-benchmark进行基准测试
pytest tests/ --benchmark-only

# 并行执行测试
pytest tests/ -n auto

# 只运行快速测试
pytest tests/ -m "fast"

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

4.7 测试失败处理策略

pytest提供了多种测试失败处理策略。

测试失败处理策略示例
python 复制代码
# test_failure_handling.py
import pytest

# 失败后停止
def test_stop_on_first_failure():
    """
    失败后停止测试
    命令:pytest -x test_failure_handling.py
    -x 或 --exitfirst:第一个失败后停止
    """
    assert True

def test_should_not_run():
    """
    这个测试不应该运行
    因为前面的测试会失败
    """
    assert False

# 最大失败次数
def test_max_failures_1():
    """
    最大失败次数测试1
    命令:pytest --maxfail=2 test_failure_handling.py
    --maxfail:达到指定失败次数后停止
    """
    assert False

def test_max_failures_2():
    """
    最大失败次数测试2
    """
    assert False

def test_max_failures_3():
    """
    最大失败次数测试3
    这个测试可能不会运行
    """
    assert True

# 继续执行失败测试
def test_continue_on_failure():
    """
    继续执行失败测试
    默认行为:继续执行所有测试
    """
    assert False

def test_should_still_run():
    """
    这个测试应该仍然运行
    """
    assert True

# 严格模式
"""
命令:pytest --strict-markers test_failure_handling.py
--strict-markers:严格标记模式,未注册的标记会报错
"""

# 捕获警告
def test_warning_as_error():
    """
    警告作为错误
    命令:pytest -W error test_failure_handling.py
    -W 或 --filterwarnings:过滤警告
    """
    import warnings
    warnings.warn("这是一个警告")
    assert True

4.8 测试标记系统(@pytest.mark)

pytest的标记系统用于组织和分类测试。

测试标记系统示例
python 复制代码
# test_markers.py
import pytest

# 基础标记
@pytest.mark.unit
def test_unit_test():
    """
    单元测试标记
    命令:pytest -m unit test_markers.py
    """
    assert True

@pytest.mark.integration
def test_integration_test():
    """
    集成测试标记
    命令:pytest -m integration test_markers.py
    """
    assert True

@pytest.mark.slow
def test_slow_test():
    """
    慢速测试标记
    命令:pytest -m slow test_markers.py
    """
    import time
    time.sleep(0.1)
    assert True

# 多个标记
@pytest.mark.unit
@pytest.mark.fast
def test_multiple_markers():
    """
    多个标记
    命令:pytest -m "unit and fast" test_markers.py
    """
    assert True

# 标记参数
@pytest.mark.parametrize("input_value", [1, 2, 3])
@pytest.mark.unit
def test_parametrize_with_marker(input_value):
    """
    参数化测试与标记结合
    """
    assert input_value > 0

# 跳过标记
@pytest.mark.skip(reason="这个测试被跳过")
def test_skip_marker():
    """
    跳过标记
    """
    assert False

# 条件跳过标记
@pytest.mark.skipif(pytest.__version__ < "7.0", reason="需要pytest 7.0+")
def test_skipif_marker():
    """
    条件跳过标记
    """
    assert True

# 预期失败标记
@pytest.mark.xfail(reason="这个测试预期失败")
def test_xfail_marker():
    """
    预期失败标记
    """
    assert False

# 标记表达式
"""
命令:pytest -m "unit and not slow" test_markers.py
使用布尔表达式组合标记
"""

4.9 自定义标记定义与注册

自定义标记需要在配置文件中注册。

自定义标记注册示例
ini 复制代码
# pytest.ini
[pytest]
markers =
    unit: 单元测试
    integration: 集成测试
    slow: 慢速测试
    fast: 快速测试
    smoke: 冒烟测试
    regression: 回归测试
    api: API测试
    ui: UI测试
    database: 数据库测试
    external: 外部依赖测试
    requires_network: 需要网络连接
python 复制代码
# test_custom_markers.py
import pytest

# 使用自定义标记
@pytest.mark.unit
def test_custom_unit():
    """
    使用自定义unit标记
    """
    assert True

@pytest.mark.smoke
def test_custom_smoke():
    """
    使用自定义smoke标记
    """
    assert True

@pytest.mark.requires_network
def test_custom_network():
    """
    使用自定义requires_network标记
    """
    import requests
    response = requests.get("https://httpbin.org/get")
    assert response.status_code == 200

@pytest.mark.database
def test_custom_database():
    """
    使用自定义database标记
    """
    assert True

4.10 标记参数与配置

标记支持参数和配置,提供更灵活的标记功能。

标记参数与配置示例
python 复制代码
# test_marker_params.py
import pytest
import time

# 带参数的标记
@pytest.mark.timeout(1)
def test_marker_with_timeout():
    """
    带超时参数的标记
    """
    time.sleep(0.5)
    assert True

@pytest.mark.flaky(reruns=3, reruns_delay=0.5)
def test_marker_with_flaky():
    """
    带重试参数的标记
    """
    import random
    if random.random() < 0.5:
        raise AssertionError("随机失败")
    assert True

# 标记配置
"""
在conftest.py中配置标记:

def pytest_configure(config):
    config.addinivalue_line("markers", "slow: 慢速测试")
    config.addinivalue_line("markers", "fast: 快速测试")
    config.addinivalue_line("markers", "unit: 单元测试")
"""

# 标记继承
class TestMarkerInheritance:
    """
    标记继承测试类
    类上的标记会应用到所有测试方法
    """

    @pytest.mark.unit
    def test_method_1(self):
        """
        测试方法1
        """
        assert True

    def test_method_2(self):
        """
        测试方法2
        这个方法没有标记,但可以继承类的标记
        """
        assert True

5. pytest钩子函数与机制

5.1 pytest钩子函数体系概述

pytest钩子函数是pytest框架的核心机制,允许开发者在测试生命周期的各个阶段插入自定义逻辑。

pytest钩子函数生命周期

pytest_configure

配置初始化
pytest_sessionstart

会话开始
pytest_collectstart

收集开始
pytest_pycollect_makeitem

创建测试项
pytest_generate_tests

生成测试
pytest_collection_modifyitems

修改测试项
pytest_collection_finish

收集完成
pytest_runtestloop

运行测试循环
pytest_runtest_setup

测试设置
pytest_runtest_call

测试调用
pytest_runtest_teardown

测试清理
pytest_runtest_logreport

记录报告
pytest_sessionfinish

会话结束
pytest_unconfigure

配置清理

钩子函数基础示例
python 复制代码
# conftest.py
"""
pytest钩子函数示例文件
所有钩子函数都应该在conftest.py中定义
"""

def pytest_configure(config):
    """
    在测试会话开始之前调用
    用于配置pytest对象和注册插件
    """
    print("\n=== pytest_configure: 测试配置开始 ===")
    
    # 注册自定义标记
    config.addinivalue_line("markers", "slow: 慢速测试")
    config.addinivalue_line("markers", "fast: 快速测试")
    
    # 添加自定义配置选项
    config.addoption("--custom-option", action="store", default="default",
                    help="自定义配置选项")

def pytest_unconfigure(config):
    """
    在测试会话结束后调用
    用于清理配置和资源
    """
    print("\n=== pytest_unconfigure: 测试配置结束 ===")

def pytest_sessionstart(session):
    """
    在测试会话开始时调用
    在pytest_configure之后
    """
    print("\n=== pytest_sessionstart: 测试会话开始 ===")
    print(f"测试会话ID: {id(session)}")

def pytest_sessionfinish(session, exitstatus):
    """
    在测试会话结束时调用
    在pytest_unconfigure之前
    """
    print("\n=== pytest_sessionfinish: 测试会话结束 ===")
    print(f"退出状态码: {exitstatus}")

def pytest_collection_modifyitems(session, config, items):
    """
    在测试收集完成后调用
    可以修改测试项列表
    """
    print(f"\n=== pytest_collection_modifyitems: 收集到 {len(items)} 个测试 ===")
    
    # 为所有测试添加默认标记
    for item in items:
        if "slow" not in item.keywords:
            item.add_marker(pytest.mark.fast)
    
    # 按标记排序测试
    items.sort(key=lambda x: "slow" in x.keywords)

def pytest_runtest_setup(item):
    """
    在每个测试执行前调用
    用于测试前的准备工作
    """
    print(f"\n=== pytest_runtest_setup: 准备测试 {item.name} ===")

def pytest_runtest_call(item):
    """
    在每个测试执行时调用
    用于监控测试执行过程
    """
    print(f"=== pytest_runtest_call: 执行测试 {item.name} ===")

def pytest_runtest_teardown(item, nextitem):
    """
    在每个测试执行后调用
    用于测试后的清理工作
    """
    print(f"=== pytest_runtest_teardown: 清理测试 {item.name} ===")
    if nextitem:
        print(f"下一个测试: {nextitem.name}")
    else:
        print("没有下一个测试")

5.2 配置相关钩子函数

配置相关的钩子函数用于控制pytest的配置行为。

配置钩子函数示例
python 复制代码
# conftest.py
import pytest

def pytest_addoption(parser):
    """
    添加命令行选项
    用于扩展pytest的命令行参数
    """
    # 添加简单选项
    parser.addoption("--slow", action="store_true", default=False,
                    help="运行慢速测试")
    
    # 添加带参数的选项
    parser.addoption("--env", action="store", default="dev",
                    choices=["dev", "staging", "prod"],
                    help="指定测试环境")
    
    # 添加互斥选项组
    group = parser.getgroup("custom", "自定义选项组")
    group.addoption("--custom-flag", action="store_true",
                   help="自定义标志")

def pytest_configure(config):
    """
    配置钩子函数
    在测试会话开始前调用
    """
    # 获取自定义选项的值
    slow_mode = config.getoption("--slow")
    env = config.getoption("--env")
    
    print(f"\n配置信息:")
    print(f"慢速模式: {slow_mode}")
    print(f"测试环境: {env}")
    
    # 根据环境变量配置测试
    import os
    os.environ["TEST_ENV"] = env
    
    # 注册自定义标记
    config.addinivalue_line("markers", "env(dev): 开发环境测试")
    config.addinivalue_line("markers", "env(staging): 预发布环境测试")
    config.addinivalue_line("markers", "env(prod): 生产环境测试")

def pytest_load_initial_conftests(early_config, parser, args):
    """
    在加载初始conftest文件时调用
    用于修改初始配置
    """
    print("\n=== pytest_load_initial_conftests: 加载初始配置 ===")
    print(f"参数: {args}")

5.3 测试收集相关钩子函数

测试收集相关的钩子函数用于控制测试的发现和收集过程。

测试收集钩子函数示例
python 复制代码
# conftest.py
import pytest

def pytest_collection_start(session):
    """
    测试收集开始时调用
    """
    print("\n=== pytest_collection_start: 开始收集测试 ===")

def pytest_collectstart(collector):
    """
    收集器开始收集时调用
    """
    print(f"=== pytest_collectstart: 收集器 {collector} 开始工作 ===")

def pytest_pycollect_makeitem(collector, name, obj):
    """
    Python收集器创建测试项时调用
    可以自定义测试项的创建逻辑
    """
    print(f"=== pytest_pycollect_makeitem: 创建测试项 {name} ===")
    
    # 自定义测试项创建逻辑
    if name.startswith("custom_test_"):
        # 创建自定义测试项
        return pytest.Function.from_parent(collector, name=name)
    
    return None

def pytest_generate_tests(metafunc):
    """
    动态生成测试
    在测试收集阶段调用
    """
    print(f"=== pytest_generate_tests: 生成测试 {metafunc.function.__name__} ===")
    
    # 检查是否有特定的fixture参数
    if "dynamic_data" in metafunc.fixturenames:
        # 动态生成参数值
        param_values = [
            {"input": 1, "expected": 2},
            {"input": 2, "expected": 4},
            {"input": 3, "expected": 6}
        ]
        
        # 参数化测试
        metafunc.parametrize("dynamic_data", param_values)

def pytest_collection_modifyitems(session, config, items):
    """
    修改收集到的测试项
    可以重新排序、过滤或修改测试项
    """
    print(f"\n=== pytest_collection_modifyitems: 修改 {len(items)} 个测试项 ===")
    
    # 按测试名称排序
    items.sort(key=lambda x: x.name)
    
    # 为测试添加标记
    for item in items:
        if "slow" in item.name:
            item.add_marker(pytest.mark.slow)
        elif "fast" in item.name:
            item.add_marker(pytest.mark.fast)
    
    # 过滤测试
    slow_mode = config.getoption("--slow")
    if not slow_mode:
        # 移除慢速测试
        items[:] = [item for item in items if "slow" not in item.keywords]

def pytest_collection_finish(session):
    """
    测试收集完成时调用
    """
    print(f"\n=== pytest_collection_finish: 收集完成,共 {len(session.items)} 个测试 ===")

5.4 测试执行相关钩子函数

测试执行相关的钩子函数用于控制测试的执行过程。

测试执行钩子函数示例
python 复制代码
# conftest.py
import pytest

def pytest_runtestloop(session):
    """
    测试循环开始时调用
    返回True表示跳过测试循环
    """
    print("\n=== pytest_runtestloop: 开始测试循环 ===")
    print(f"待执行测试数量: {len(session.items)}")
    return None  # 继续执行测试循环

def pytest_runtest_protocol(item, nextitem):
    """
    测试协议执行时调用
    返回True表示跳过测试执行
    """
    print(f"\n=== pytest_runtest_protocol: 执行测试 {item.name} ===")
    return None  # 继续执行测试

def pytest_runtest_setup(item):
    """
    测试设置阶段
    在测试执行前调用
    """
    print(f"=== pytest_runtest_setup: 设置测试 {item.name} ===")
    
    # 检查测试依赖
    if hasattr(item, "obj") and hasattr(item.obj, "__depends__"):
        dependencies = item.obj.__depends__
        print(f"测试依赖: {dependencies}")

def pytest_runtest_call(item):
    """
    测试调用阶段
    实际执行测试函数
    """
    print(f"=== pytest_runtest_call: 调用测试 {item.name} ===")

def pytest_runtest_teardown(item, nextitem):
    """
    测试清理阶段
    在测试执行后调用
    """
    print(f"=== pytest_runtest_teardown: 清理测试 {item.name} ===")
    if nextitem:
        print(f"下一个测试: {nextitem.name}")

def pytest_runtest_logreport(report):
    """
    记录测试报告
    在测试的每个阶段调用
    """
    print(f"=== pytest_runtest_logreport: {report.when} ===")
    print(f"测试节点: {report.nodeid}")
    print(f"测试结果: {report.outcome}")
    
    if report.failed:
        print(f"失败原因: {report.longrepr}")
    elif report.skipped:
        print(f"跳过原因: {report.longrepr}")

def pytest_runtest_makereport(item, call):
    """
    创建测试报告
    可以自定义报告内容
    """
    print(f"=== pytest_runtest_makereport: 创建报告 {item.name} ===")
    
    # 获取默认报告
    report = pytest.TestReport.from_item_and_call(item, call)
    
    # 添加自定义信息
    report.custom_info = f"自定义信息: {item.name}"
    
    return report

5.5 报告相关钩子函数

报告相关的钩子函数用于控制测试报告的生成和输出。

报告钩子函数示例
python 复制代码
# conftest.py
import pytest

def pytest_report_header(config, startdir):
    """
    添加报告头部信息
    在测试开始时显示
    """
    lines = []
    lines.append("=== 自定义报告头部 ===")
    lines.append(f"测试目录: {startdir}")
    lines.append(f"Python版本: {config.pythonversion}")
    lines.append(f"pytest版本: {pytest.__version__}")
    return "\n".join(lines)

def pytest_report_teststatus(report, config):
    """
    自定义测试状态显示
    返回状态字符、单词和颜色
    """
    print(f"=== pytest_report_teststatus: {report.nodeid} ===")
    
    if report.when == "call":
        if report.passed:
            return "✓", "PASSED", {"green": True}
        elif report.failed:
            return "✗", "FAILED", {"red": True}
        elif report.skipped:
            return "⊘", "SKIPPED", {"yellow": True}
    
    return None

def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """
    终端摘要报告
    在测试结束时显示
    """
    print("\n=== pytest_terminal_summary: 终端摘要 ===")
    
    # 获取测试统计信息
    stats = terminalreporter.stats
    total = sum(len(stats.get(key, [])) for key in ["passed", "failed", "skipped"])
    passed = len(stats.get("passed", []))
    failed = len(stats.get("failed", []))
    skipped = len(stats.get("skipped", []))
    
    print(f"总计: {total} 个测试")
    print(f"通过: {passed} 个")
    print(f"失败: {failed} 个")
    print(f"跳过: {skipped} 个")
    print(f"通过率: {passed/total*100:.1f}%" if total > 0 else "通过率: 0%")

def pytest_html_results_summary(prefix, summary, postfix):
    """
    HTML报告摘要
    用于pytest-html插件
    """
    prefix.extend([
        "<h2>自定义HTML报告摘要</h2>",
        "<p>这是自定义的HTML报告摘要内容</p>"
    ])

def pytest_html_results_table_row(report, cells):
    """
    HTML报告表格行
    可以自定义表格行的显示
    """
    if report.passed:
        cells[-1].append(pytest.mark.passed)
    elif report.failed:
        cells[-1].append(pytest.mark.failed)

5.6 调试相关钩子函数

调试相关的钩子函数用于增强调试功能。

调试钩子函数示例
python 复制代码
# conftest.py
import pytest

def pytest_exception_interact(node, call, report):
    """
    异常交互
    当测试抛出异常时调用
    """
    print(f"\n=== pytest_exception_interact: 异常交互 ===")
    print(f"测试节点: {node.nodeid}")
    print(f"异常类型: {type(call.excinfo.value).__name__}")
    print(f"异常信息: {str(call.excinfo.value)}")

def pytest_internalerror(excrepr):
    """
    内部错误
    当pytest内部发生错误时调用
    """
    print("\n=== pytest_internalerror: pytest内部错误 ===")
    print(f"错误信息: {excrepr}")

def pytest_keyboard_interrupt(excinfo):
    """
    键盘中断
    当用户按下Ctrl+C时调用
    """
    print("\n=== pytest_keyboard_interrupt: 用户中断测试 ===")
    print(f"中断信息: {excinfo}")

def pytest_enter_pdb(config):
    """
    进入PDB调试器
    当测试进入调试模式时调用
    """
    print("\n=== pytest_enter_pdb: 进入调试模式 ===")

def pytest_leave_pdb(config):
    """
    离开PDB调试器
    当测试离开调试模式时调用
    """
    print("\n=== pytest_leave_pdb: 离开调试模式 ===")

5.7 钩子函数参数详解

钩子函数接收不同的参数,提供了丰富的上下文信息。

钩子函数参数示例
python 复制代码
# conftest.py
import pytest

def pytest_configure(config):
    """
    config参数详解
    config对象提供了pytest的配置信息
    """
    print("\n=== pytest_configure: config参数详解 ===")
    
    # 获取命令行选项
    verbose = config.getoption("verbose")
    print(f"详细模式: {verbose}")
    
    # 获取插件信息
    plugins = config.pluginmanager.get_plugins()
    print(f"已加载插件数量: {len(plugins)}")
    
    # 获取ini配置
    ini_config = config.getini("markers")
    print(f"标记配置: {ini_config}")
    
    # 获取根目录
    rootdir = config.rootdir
    print(f"根目录: {rootdir}")

def pytest_collection_modifyitems(session, config, items):
    """
    session、config、items参数详解
    """
    print("\n=== pytest_collection_modifyitems: 参数详解 ===")
    
    # session参数
    print(f"session对象ID: {id(session)}")
    print(f"session.items数量: {len(session.items)}")
    
    # config参数
    print(f"config对象ID: {id(config)}")
    
    # items参数
    print(f"items类型: {type(items)}")
    print(f"items长度: {len(items)}")
    
    # 遍历items
    for i, item in enumerate(items[:3]):  # 只显示前3个
        print(f"item {i}:")
        print(f"  name: {item.name}")
        print(f"  nodeid: {item.nodeid}")
        print(f"  keywords: {list(item.keywords.keys())}")

def pytest_runtest_logreport(report):
    """
    report参数详解
    report对象包含测试的详细信息
    """
    print(f"\n=== pytest_runtest_logreport: report参数详解 ===")
    
    # 基本信息
    print(f"nodeid: {report.nodeid}")
    print(f"when: {report.when}")  # setup, call, teardown
    print(f"outcome: {report.outcome}")  # passed, failed, skipped
    
    # 时间信息
    print(f"duration: {report.duration:.3f}秒")
    
    # 失败信息
    if report.failed:
        print(f"longrepr: {report.longrepr}")
        print(f"sections: {report.sections}")
    
    # 跳过信息
    if report.skipped:
        print(f"skip原因: {report.longrepr}")

def pytest_runtest_setup(item):
    """
    item参数详解
    item对象代表一个测试项
    """
    print(f"\n=== pytest_runtest_setup: item参数详解 ===")
    
    # 基本信息
    print(f"name: {item.name}")
    print(f"nodeid: {item.nodeid}")
    print(f"parent: {item.parent}")
    
    # 关键字(标记)
    print(f"keywords: {list(item.keywords.keys())}")
    
    # fixture信息
    print(f"fixturenames: {item.fixturenames}")
    
    # 测试函数信息
    if hasattr(item, "obj"):
        print(f"function: {item.obj}")
        print(f"module: {item.module}")

5.8 钩子函数执行顺序

钩子函数有明确的执行顺序,理解这个顺序对于正确使用钩子函数至关重要。

钩子函数执行顺序示例
python 复制代码
# conftest.py
import pytest

def pytest_configure(config):
    print("\n【1】pytest_configure: 配置初始化")

def pytest_load_initial_conftests(early_config, parser, args):
    print("【2】pytest_load_initial_conftests: 加载初始配置")

def pytest_sessionstart(session):
    print("【3】pytest_sessionstart: 会话开始")

def pytest_collection_start(session):
    print("【4】pytest_collection_start: 收集开始")

def pytest_collectstart(collector):
    print(f"【5】pytest_collectstart: 收集器 {collector} 开始")

def pytest_pycollect_makeitem(collector, name, obj):
    print(f"【6】pytest_pycollect_makeitem: 创建测试项 {name}")
    return None

def pytest_generate_tests(metafunc):
    print(f"【7】pytest_generate_tests: 生成测试 {metafunc.function.__name__}")

def pytest_collection_modifyitems(session, config, items):
    print(f"【8】pytest_collection_modifyitems: 修改 {len(items)} 个测试项")

def pytest_collection_finish(session):
    print(f"【9】pytest_collection_finish: 收集完成")

def pytest_runtestloop(session):
    print("【10】pytest_runtestloop: 测试循环开始")
    return None

def pytest_runtest_protocol(item, nextitem):
    print(f"【11】pytest_runtest_protocol: 执行测试 {item.name}")
    return None

def pytest_runtest_setup(item):
    print(f"【12】pytest_runtest_setup: 设置测试 {item.name}")

def pytest_runtest_call(item):
    print(f"【13】pytest_runtest_call: 调用测试 {item.name}")

def pytest_runtest_teardown(item, nextitem):
    print(f"【14】pytest_runtest_teardown: 清理测试 {item.name}")

def pytest_runtest_logreport(report):
    print(f"【15】pytest_runtest_logreport: {report.when} - {report.outcome}")

def pytest_runtest_makereport(item, call):
    print(f"【16】pytest_runtest_makereport: 创建报告 {item.name}")
    return pytest.TestReport.from_item_and_call(item, call)

def pytest_sessionfinish(session, exitstatus):
    print(f"【17】pytest_sessionfinish: 会话结束,退出码 {exitstatus}")

def pytest_unconfigure(config):
    print("【18】pytest_unconfigure: 配置清理")

5.9 自定义钩子函数实现

可以创建自定义钩子函数,扩展pytest的功能。

自定义钩子函数示例
python 复制代码
# conftest.py
import pytest

# 定义自定义钩子函数
def pytest_addhooks(pluginmanager):
    """
    注册自定义钩子函数
    """
    pluginmanager.add_hookspecs(CustomHooks)

class CustomHooks:
    """
    自定义钩子函数规范
    """
    
    @staticmethod
    def pytest_custom_test_start(item):
        """
        自定义测试开始钩子
        在测试开始时调用
        """
        pass
    
    @staticmethod
    def pytest_custom_test_end(item, result):
        """
        自定义测试结束钩子
        在测试结束时调用
        """
        pass

# 实现自定义钩子函数
@pytest.hookimpl(tryfirst=True)
def pytest_custom_test_start(item):
    """
    实现自定义测试开始钩子
    """
    print(f"\n[自定义] 测试开始: {item.name}")
    print(f"[自定义] 测试ID: {item.nodeid}")

@pytest.hookimpl(trylast=True)
def pytest_custom_test_end(item, result):
    """
    实现自定义测试结束钩子
    """
    print(f"[自定义] 测试结束: {item.name}")
    print(f"[自定义] 测试结果: {result}")

# 在测试中使用自定义钩子
def pytest_runtest_setup(item):
    """
    调用自定义钩子函数
    """
    item.config.hook.pytest_custom_test_start(item=item)

def pytest_runtest_teardown(item, nextitem):
    """
    调用自定义钩子函数
    """
    result = "completed"
    item.config.hook.pytest_custom_test_end(item=item, result=result)

5.10 钩子函数最佳实践

使用钩子函数时需要遵循一些最佳实践。

钩子函数最佳实践示例
python 复制代码
# conftest.py
import pytest
import time
import logging

# 最佳实践1:使用tryfirst和trylast控制执行顺序
@pytest.hookimpl(tryfirst=True)
def pytest_configure_first(config):
    """
    最早执行的配置钩子
    用于初始化基础配置
    """
    print("\n[最早] pytest_configure_first: 基础配置初始化")
    
    # 初始化日志
    logging.basicConfig(level=logging.INFO)

@pytest.hookimpl(trylast=True)
def pytest_configure_last(config):
    """
    最晚执行的配置钩子
    用于最终配置确认
    """
    print("[最晚] pytest_configure_last: 配置确认完成")

# 最佳实践2:使用hookwrapper包装钩子函数
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """
    使用hookwrapper包装钩子函数
    可以在钩子函数前后添加额外逻辑
    """
    outcome = yield
    
    # 获取原始报告
    report = outcome.get_result()
    
    # 添加自定义信息
    report.custom_info = f"测试时间: {time.strftime('%Y-%m-%d %H:%M:%S')}"
    
    # 记录测试信息
    logging.info(f"测试 {item.name} 执行完成,结果: {report.outcome}")

# 最佳实践3:使用可选钩子函数
@pytest.hookimpl(optionalhook=True)
def pytest_html_results_table_row(report, cells):
    """
    可选钩子函数
    只在pytest-html插件存在时执行
    """
    if hasattr(report, "custom_info"):
        cells.append(report.custom_info)

# 最佳实践4:避免在钩子函数中执行耗时操作
@pytest.hookimpl(tryfirst=True)
def pytest_configure_fast(config):
    """
    快速配置钩子
    避免执行耗时操作
    """
    # 快速配置,不执行耗时操作
    config.addinivalue_line("markers", "fast: 快速测试")

# 最佳实践5:使用条件判断执行钩子函数
@pytest.hookimpl(tryfirst=True)
def pytest_configure_conditional(config):
    """
    条件配置钩子
    根据条件执行不同配置
    """
    verbose = config.getoption("verbose")
    
    if verbose:
        print("详细模式:启用详细日志")
    else:
        print("普通模式:启用简洁日志")

# 最佳实践6:使用异常处理保护钩子函数
@pytest.hookimpl(tryfirst=True)
def pytest_configure_safe(config):
    """
    安全配置钩子
    使用异常处理保护钩子函数
    """
    try:
        # 尝试配置
        config.addinivalue_line("markers", "safe: 安全测试")
    except Exception as e:
        logging.error(f"配置钩子出错: {e}")
        # 不抛出异常,避免影响pytest执行

# 最佳实践7:使用配置选项控制钩子函数行为
def pytest_addoption(parser):
    """
    添加配置选项
    """
    parser.addoption("--enable-custom-hooks", action="store_true",
                    default=False, help="启用自定义钩子函数")

@pytest.hookimpl(tryfirst=True)
def pytest_configure_with_option(config):
    """
    带选项的配置钩子
    根据配置选项决定是否执行
    """
    if config.getoption("--enable-custom-hooks"):
        print("自定义钩子函数已启用")
    else:
        print("自定义钩子函数已禁用")

# 最佳实践8:使用钩子函数收集测试统计信息
class TestStatistics:
    """
    测试统计信息收集器
    """
    def __init__(self):
        self.total_tests = 0
        self.passed_tests = 0
        self.failed_tests = 0
        self.skipped_tests = 0
        self.test_duration = 0.0

stats = TestStatistics()

@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems_with_stats(session, config, items):
    """
    收集测试统计信息
    """
    stats.total_tests = len(items)
    print(f"收集到 {stats.total_tests} 个测试")

@pytest.hookimpl(trylast=True)
def pytest_runtest_logreport_with_stats(report):
    """
    记录测试统计信息
    """
    if report.when == "call":
        stats.test_duration += report.duration
        
        if report.passed:
            stats.passed_tests += 1
        elif report.failed:
            stats.failed_tests += 1
        elif report.skipped:
            stats.skipped_tests += 1

@pytest.hookimpl(trylast=True)
def pytest_terminal_summary_with_stats(terminalreporter, exitstatus, config):
    """
    显示测试统计信息
    """
    print("\n=== 测试统计信息 ===")
    print(f"总测试数: {stats.total_tests}")
    print(f"通过: {stats.passed_tests}")
    print(f"失败: {stats.failed_tests}")
    print(f"跳过: {stats.skipped_tests}")
    print(f"总耗时: {stats.test_duration:.3f}秒")
    print(f"平均耗时: {stats.test_duration/stats.total_tests:.3f}秒" if stats.total_tests > 0 else "平均耗时: 0秒")

# 最佳实践9:使用钩子函数实现测试依赖管理
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_setup_with_dependency(item):
    """
    实现测试依赖管理
    """
    if hasattr(item, "obj") and hasattr(item.obj, "__depends__"):
        dependencies = item.obj.__depends__
        
        # 检查依赖测试是否通过
        for dep in dependencies:
            dep_item = item.session.items.get(dep)
            if dep_item and dep_item._report.failed:
                pytest.skip(f"依赖测试 {dep} 失败,跳过当前测试")

# 最佳实践10:使用钩子函数实现测试重试机制
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_makereport_with_retry(item, call):
    """
    实现测试重试机制
    """
    if call.when == "call" and call.excinfo:
        # 获取重试次数配置
        max_retries = item.config.getoption("--max-retries", default=0)
        
        if max_retries > 0:
            # 实现重试逻辑
            print(f"测试失败,准备重试(最多 {max_retries} 次)")
相关推荐
明月_清风1 小时前
FastAPI 从入门到实战:3 分钟构建高性能异步 API
后端·python·fastapi
笨拙的老猴子1 小时前
[特殊字符] Java GC机制详解:G1、ZGC、Shenandoah全面解析与版本演进对比
java·开发语言
bellus-1 小时前
ubuntu26测试win10的ollama大模型性能
python
水木流年追梦1 小时前
大模型入门-Reward 奖励模型训练
开发语言·python·算法·leetcode·正则表达式
JavaWeb学起来1 小时前
Python学习教程(六)数据结构List(列表)
数据结构·python·python基础·python教程
liuyunshengsir1 小时前
PyTorch 动态量化(Dynamic Quantization)
人工智能·pytorch·python
电子云与长程纠缠1 小时前
UE5制作六边形包裹球体效果
开发语言·python·ue5
砍材农夫1 小时前
物联网 基于netty构建mqtt协议规范(遗嘱与保留消息)
java·开发语言·物联网·netty
DFT计算杂谈1 小时前
KPROJ编译教程
java·前端·python·算法·conda
froginwe112 小时前
Python3 迭代器与生成器
开发语言