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} 次)")