本教程深入讲解 pytest 的核心钩子函数,包括每个钩子的功能、参数解析、使用场景、实战案例和注意事项,助你全面掌握测试框架定制能力。
一、测试初始化钩子
1.1 pytest_addoption(parser)
功能:添加自定义命令行选项和配置
参数:
parser
(pytest.Parser): 命令行解析器对象
场景:
- 定义环境切换参数
- 配置测试模式开关
- 设置全局超时时间
案例:
def pytest_addoption(parser):
# 添加 --env 选项
parser.addoption("--env", action="store", default="dev",
choices=["dev", "stage", "prod"],
help="选择测试环境")
# 添加 ini 配置项
parser.addini("api_timeout", default=30, type="int",
help="API请求超时时间(秒)")
# 添加布尔选项
parser.addoption("--quick", action="store_true", default=False,
help="快速模式(跳过长耗时测试)")
注意事项:
- 在
conftest.py
或插件中定义 - 使用
getoption()
获取命令行参数值 - 使用
getini()
获取配置文件中的值
1.2 pytest_configure(config)
功能:配置初始化完成后调用的钩子
参数:
config
(pytest.Config): 配置对象
场景:
- 注册自定义标记
- 初始化全局资源
- 修改配置参数
案例:
def pytest_configure(config):
# 添加标记描述
config.addinivalue_line("markers", "smoke: 冒烟测试")
# 创建测试结果目录
os.makedirs("test-results", exist_ok=True)
# 设置环境变量
env = config.getoption("--env")
os.environ["TEST_ENV"] = env
# 动态添加插件
if config.getoption("--quick"):
config.pluginmanager.import_plugin("plugins.quick_mode")
注意事项:
- 在多进程运行(xdist)时,每个工作进程都会调用
- 避免在此钩子中执行耗时操作
- 修改配置后需确保向后兼容
二、测试收集钩子
2.1 pytest_collection_modifyitems(items, config)
功能:修改收集到的测试项
参数:
items
(List[pytest.Item]): 测试项列表config
(pytest.Config): 配置对象
场景:
- 测试项排序
- 动态添加/删除测试
- 批量添加标记
- 基于条件过滤测试
案例:
def pytest_collection_modifyitems(items, config):
# 根据测试名称排序
items.sort(key=lambda x: x.name)
# 添加环境标记
for item in items:
if "api" in item.nodeid:
item.add_marker("api")
# 快速模式下跳过长测试
if config.getoption("--quick"):
quick_items = []
for item in items:
if not item.get_closest_marker("long"):
quick_items.append(item)
items[:] = quick_items
注意事项:
- 直接修改
items
列表(不要创建新列表) - 使用
item.add_marker
添加标记 - 使用
items.remove()
删除测试项
2.2 pytest_ignore_collect(path, config)
功能:决定是否忽略收集路径
参数:
path
(py.path.local): 文件系统路径config
(pytest.Config): 配置对象
场景:
- 忽略指定目录
- 跳过临时文件
- 基于环境过滤测试目录
案例:
def pytest_ignore_collect(path, config):
# 忽略备份文件
if path.basename.startswith("backup_"):
return True
# 在非dev环境下忽略开发测试
if "dev_tests" in str(path) and config.getoption("--env") != "dev":
return True
三、测试执行钩子
3.1 pytest_runtest_setup(item)
功能:测试设置阶段钩子
参数:
item
(pytest.Item): 当前测试项
场景:
- 测试前置准备工作
- 条件跳过检查
- 资源分配
案例:
def pytest_runtest_setup(item):
# 数据库连接检查
if item.get_closest_marker("db"):
if not database.is_connected():
pytest.skip("数据库不可用")
# 为性能测试分配独立资源
if item.get_closest_marker("perf"):
allocate_performance_resource()
注意事项:
- 此钩子中抛出的任何异常都会被视为测试错误
- 若在此处跳过测试,测试状态为
skipped
3.2 pytest_runtest_call(item)
功能:测试调用阶段钩子
参数:
item
(pytest.Item): 当前测试项
场景:
- 替代默认测试执行
- 添加监控逻辑
- 重试机制实现
案例:
def pytest_runtest_call(item):
# 自定义重试逻辑
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
# 调用原始测试函数
item.obj()
break
except Exception as e:
if attempt == max_retries:
raise
print(f"重试 #{attempt} 失败: {e}")
注意事项:
- 如果要自定义测试执行,需要在此钩子中调用
item.obj()
- 默认会执行测试函数,除非覆盖此钩子
3.3 pytest_runtest_teardown(item, nextitem)
功能:测试清理阶段钩子
参数:
item
(pytest.Item): 当前测试项nextitem
(pytest.Item): 下一个测试项
场景:
- 资源清理
- 状态重置
- 数据持久化
案例:
def pytest_runtest_teardown(item, nextitem):
# 清理数据库测试数据
if item.get_closest_marker("db"):
database.clean_test_data()
# 保存性能测试结果
if item.get_closest_marker("perf"):
save_performance_metrics(item.name)
注意事项:
- 即使测试失败也会调用此钩子
- 清理操作应幂等(可重复执行)
四、报告与日志钩子
4.1 pytest_report_header(config)
功能:定制报告头部信息
参数:
config
(pytest.Config): 配置对象
场景:
- 显示环境信息
- 添加项目元数据
- 输出重要配置
案例:
def pytest_report_header(config):
"""添加3行报告头部信息"""
env = config.getoption("--env")
python_ver = platform.python_version()
os_info = f"{platform.system()} {platform.release()}"
return [
f"环境: {env} | Python: {python_ver} | OS: {os_info}",
f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"----------------------------------------"
]
4.2 pytest_runtest_logreport(report)
功能:处理测试日志报告
参数:
report
(TestReport): 测试报告对象
场景:
- 自定义日志格式
- 失败重试
- 结果通知
- 测试时间统计
案例:
TEST_TIMINGS = {}
def pytest_runtest_logreport(report):
# 记录测试耗时
if report.when == "call" and report.outcome == "passed":
TEST_TIMINGS[report.nodeid] = report.duration
# 失败时自动收集日志
if report.failed:
save_failure_details(report)
# 通知测试失败
if report.failed and "critical" in report.keywords:
send_slack_alert(f"关键测试失败: {report.nodeid}")
报告对象重要属性:
属性 | 说明 |
---|---|
when | 测试阶段:setup/call/teardown |
outcome | 结果:passed/failed/skipped |
nodeid | 测试唯一标识 |
duration | 执行时间(秒) |
caplog | 捕获的日志 |
capstderr | 标准错误输出 |
longrepr | 失败详细信息 |
4.3 pytest_terminal_summary(terminalreporter, exitstatus, config)
功能:生成终端报告摘要
参数:
terminalreporter
(TerminalReporter): 终端报告对象exitstatus
(int): 退出状态码config
(pytest.Config): 配置对象
场景:
- 添加性能统计
- 显示失败摘要
- 输出资源使用情况
- 生成自定义报告
案例:
def pytest_terminal_summary(terminalreporter, exitstatus, config):
# 添加自定义统计部分
terminalreporter.write_sep("=", "性能统计")
passed_reports = terminalreporter.getreports("passed")
if passed_reports:
avg_time = sum(r.duration for r in passed_reports) / len(passed_reports)
terminalreporter.write_line(f"平均执行时间: {avg_time:.3f}秒")
# 生成HTML报告
if config.getoption("--report"):
generate_html_report(terminalreporter)
五、Fixture 生命周期钩子
5.1 pytest_fixture_setup(fixturedef, request)
功能:在fixture设置前调用
参数:
fixturedef
(FixtureDef): fixture定义对象request
(SubRequest): fixture请求上下文
场景:
- 动态修改fixture参数
- 前置校验
- 资源预加载
案例:
def pytest_fixture_setup(fixturedef, request):
# 为数据库fixture增加连接池大小
if fixturedef.argname == "db_connection":
if request.param.get("high_load"):
fixturedef.func = partial(fixturedef.func, pool_size=50)
# 验证环境变量
if fixturedef.argname == "api_client":
if not os.getenv("API_KEY"):
pytest.skip("缺少API_KEY环境变量")
5.2 pytest_fixture_post_finalizer(fixturedef, request)
功能:在fixture清理后调用
参数:
fixturedef
(FixtureDef): fixture定义对象request
(SubRequest): fixture请求上下文
场景:
- 清理残留资源
- 验证清理结果
- 持久化状态
案例:
def pytest_fixture_post_finalizer(fixturedef, request):
# 确保临时文件删除
if fixturedef.argname == "temp_file":
if os.path.exists(request.param):
raise RuntimeError("临时文件未被正确删除")
# 记录fixture使用情况
if "database" in fixturedef.argname:
log_fixture_usage(fixturedef.argname, request.node.name)
六、高级钩子技巧
6.1 钩子执行顺序控制
class PerformancePlugin:
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_protocol(self, item):
# 最先执行
item.start_time = time.time()
@pytest.hookimpl(trylast=True)
def pytest_runtest_teardown(self, item, nextitem):
# 最后执行
item.duration = time.time() - item.start_time
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(self, item):
# 包裹原有钩子
before = resource_usage()
yield
after = resource_usage()
item.mem_used = after - before
6.2 跨钩子状态共享
# 使用插件类管理状态
class StateRecorder:
def __init__(self):
self.test_results = {}
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_logreport(self, report):
if report.when == "call":
self.test_results[report.nodeid] = report.outcome
@pytest.hookimpl(trylast=True)
def pytest_terminal_summary(self, terminalreporter):
terminalreporter.write_line(f"总测试数: {len(self.test_results)}")
def pytest_configure(config):
# 注册状态记录器
config.pluginmanager.register(StateRecorder())
七、企业级应用案例
7.1 智能测试排序系统
# plugins/smart_ordering.py
class SmartOrdering:
def __init__(self):
self.failed_first = True
self.last_failed = []
@pytest.hookimpl(trylast=True)
def pytest_sessionstart(self, session):
# 加载上次失败记录
if os.path.exists("last_failed.json"):
with open("last_failed.json") as f:
self.last_failed = json.load(f)
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(self, items):
# 上次失败测试优先
if self.failed_first and self.last_failed:
failed_items = [item for item in items if item.nodeid in self.last_failed]
other_items = [item for item in items if item.nodeid not in self.last_failed]
items[:] = failed_items + other_items
# 按测试依赖排序
items.sort(key=self.dependency_order)
def dependency_order(self, item):
# 解析测试依赖关系
dependency = getattr(item.function, "__dependency", None)
return (dependency.priority if dependency else 100)
@pytest.hookimpl(trylast=True)
def pytest_sessionfinish(self, session):
# 记录失败测试
self.last_failed = [
item.nodeid for item in session.items
if getattr(item, "failed", False)
]
with open("last_failed.json", "w") as f:
json.dump(self.last_failed, f)
# 注册插件
def pytest_configure(config):
config.pluginmanager.register(SmartOrdering())
八、钩子调试技巧
8.1 钩子调用追踪
# conftest.py
def pytest_cmdline_main(config):
# 启用钩子追踪
config.pluginmanager.enable_tracing()
# 保存钩子调用日志
with open("hook_trace.log", "w") as f:
config.pluginmanager.trace.root.setwriter(f.write)
8.2 钩子异常捕获
def pytest_exception_interact(node, call, report):
"""处理测试失败时的异常"""
# 获取异常信息
exc_info = call.excinfo
exc_type = exc_info.type.__name__
exc_msg = str(exc_info.value)
# 截图Web测试
if "browser" in node.fixturenames:
browser = node.funcargs["browser"]
browser.save_screenshot(f"failure_{node.name}.png")
# 日志分析
print(f"测试失败: {node.name}")
print(f"异常类型: {exc_type}")
print(f"异常信息: {exc_msg}")
print(f"堆栈跟踪:\n{exc_info.traceback}")
完整钩子函数参考表:
钩子类别 钩子函数 关键参数 初始化 pytest_addoption parser 初始化 pytest_configure config 收集 pytest_collection session 收集 pytest_collection_modifyitems items, config 执行 pytest_runtest_setup item 执行 pytest_runtest_call item 执行 pytest_runtest_teardown item, nextitem Fixture pytest_fixture_setup fixturedef, request Fixture pytest_fixture_post_finalizer fixturedef, request 报告 pytest_report_header config 报告 pytest_runtest_logreport report 报告 pytest_terminal_summary terminalreporter, exitstatus, config 异常 pytest_exception_interact node, call, report 会话 pytest_sessionstart session 会话 pytest_sessionfinish session, exitstatus