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} 次)")
相关推荐
bearpping2 小时前
java进阶知识点
java·开发语言
杰杰7982 小时前
Python面向对象——类的魔法方法
开发语言·python
Joker Zxc2 小时前
【前端基础(Javascript部分)】6、用JavaScript的递归函数和for循环,计算斐波那契数列的第 n 项值
开发语言·前端·javascript
kkkkatoq2 小时前
JAVA中的IO操作
java·开发语言
Highcharts.js2 小时前
React 图表如何实现下钻(Drilldown)效果
开发语言·前端·javascript·react.js·前端框架·数据可视化·highcharts
chushiyunen2 小时前
python中的魔术方法(双下划线)
前端·javascript·python
小罗和阿泽2 小时前
接口测试系列 接口自动化测试 pytest框架(二)
pytest
s09071362 小时前
【声纳成像】基于滑动子孔径与加权拼接的条带式多子阵SAS连续成像(MATLAB仿真)
开发语言·算法·matlab·合成孔径声呐·后向投影算法·条带拼接
深蓝轨迹2 小时前
@Autowired与@Resource:Spring依赖注入注解核心差异剖析
java·python·spring·注解