钩子函数运行原理
Pytest 的钩子函数机制是基于插件系统的,其核心原理如下:
-
插件架构:Pytest 通过 setuptools 入口点发现插件,所有钩子函数都通过插件形式实现
-
钩子调用点:Pytest 在测试执行的各个阶段预留了特定的调用点(hook点)
-
执行顺序:钩子函数按插件注册顺序执行,除非使用
tryfirst或trylast标记 -
自动发现:Pytest 会自动发现
conftest.py文件和已安装插件中的钩子函数 -
参数传递:每个钩子函数都有特定的参数签名,Pytest 会自动传入相关参数
Pytest 钩子函数常用场景及示例代码
一、测试环境准备与清理
1. 全局测试环境准备 (pytest_configure + pytest_sessionstart)
# conftest.py
def pytest_configure(config):
"""测试配置初始化"""
print("初始化测试配置...")
# 添加自定义标记
config.addinivalue_line(
"markers",
"db: 需要数据库连接的测试"
)
def pytest_sessionstart(session):
"""测试会话开始前执行"""
print("创建测试数据库连接池...")
session.db_pool = create_db_pool()
def pytest_sessionfinish(session, exitstatus):
"""测试会话结束后执行"""
print("关闭数据库连接池...")
session.db_pool.close()
使用场景:
- 全局资源初始化(数据库连接池、Redis连接等)
- 添加自定义标记说明
- 全局配置设置
二、测试用例动态生成与修改
2. 动态参数化测试 (pytest_generate_tests)
# conftest.py
def pytest_generate_tests(metafunc):
"""根据条件动态生成测试用例"""
if "user_role" in metafunc.fixturenames:
roles = ["admin", "editor", "viewer"]
if metafunc.config.getoption("--include-guest"):
roles.append("guest")
metafunc.parametrize("user_role", roles)
使用场景:
- 根据命令行参数动态生成测试用例
- 从外部文件(YAML/JSON)加载测试数据
- 条件性参数化
3. 测试收集后修改 (pytest_collection_modifyitems)
# conftest.py
def pytest_collection_modifyitems(config, items):
"""修改收集到的测试项"""
# 1. 跳过标记为slow且未指定--run-slow的测试
if not config.getoption("--run-slow"):
skip_slow = pytest.mark.skip(reason="需要--run-slow选项来运行慢速测试")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)
# 2. 按测试名称排序
items.sort(key=lambda x: x.nodeid)
使用场景:
- 测试筛选与跳过
- 测试执行顺序调整
- 批量添加标记
三、测试执行过程控制
4. 测试前置后置操作 (pytest_runtest_setup + pytest_runtest_teardown)
# conftest.py
def pytest_runtest_setup(item):
"""单个测试执行前的设置"""
if "db" in item.keywords:
item.db_conn = item.session.db_pool.get_connection()
print(f"获取数据库连接 {item.db_conn} 用于测试 {item.nodeid}")
def pytest_runtest_teardown(item, nextitem):
"""单个测试执行后的清理"""
if hasattr(item, 'db_conn'):
item.session.db_pool.release_connection(item.db_conn)
print(f"释放数据库连接 {item.db_conn}")
使用场景:
- 测试专用资源分配与释放
- 测试环境检查
- 测试前后状态记录
5. 测试失败重试机制 (pytest_runtest_makereport)
# conftest.py
def pytest_runtest_makereport(item, call):
"""生成测试报告并实现失败重试"""
if call.when == "call" and call.excinfo is not None:
max_retries = 3
retry = getattr(item, "retry", 0)
if retry < max_retries:
item.retry = retry + 1
pytest.runtest.runtestprotocol(item, nextitem=None, log=False)
使用场景:
- 失败自动重试
- 测试结果增强报告
- 异常捕获与处理
四、自定义命令行选项
6. 添加命令行选项 (pytest_addoption)
# conftest.py
def pytest_addoption(parser):
"""添加自定义命令行选项"""
parser.addoption(
"--env",
action="store",
default="test",
choices=["test", "staging", "prod"],
help="指定测试环境"
)
parser.addoption(
"--run-slow",
action="store_true",
default=False,
help="运行标记为slow的测试"
)
def pytest_configure(config):
"""使用命令行选项"""
env = config.getoption("--env")
os.environ["TEST_ENV"] = env
print(f"当前测试环境: {env}")
使用场景:
- 环境切换控制
- 测试行为开关
- 运行时参数传递
五、测试报告定制
7. 定制终端报告 (pytest_terminal_summary)
# conftest.py
def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""在终端报告中添加自定义摘要"""
duration = time.time() - terminalreporter._sessionstarttime
terminalreporter.section("测试结果摘要")
terminalreporter.line(f"总运行时间: {duration:.2f}秒")
stats = terminalreporter.stats
passed = len(stats.get('passed', []))
failed = len(stats.get('failed', []))
skipped = len(stats.get('skipped', []))
terminalreporter.line(f"通过: {passed} | 失败: {failed} | 跳过: {skipped}")
if failed > 0:
terminalreporter.line("\n失败测试列表:")
for report in stats['failed']:
terminalreporter.line(f" - {report.nodeid}")
使用场景:
- 自定义测试结果摘要
- 关键指标统计
- 失败测试重点展示
六、综合实战案例
8. API 测试自动化框架集成
# conftest.py
import pytest
import requests
from datetime import datetime
def pytest_addoption(parser):
parser.addoption("--base-url", default="http://localhost:8000",
help="API基础URL")
@pytest.fixture(scope="session")
def api_client(request):
"""创建API客户端会话"""
base_url = request.config.getoption("--base-url")
session = requests.Session()
session.base_url = base_url
# 认证处理
if request.config.getoption("--auth"):
session.headers.update({
"Authorization": f"Bearer {request.config.getoption('--auth')}"
})
yield session
session.close()
def pytest_runtest_setup(item):
"""API测试前置处理"""
if "api" in item.keywords:
item.start_time = datetime.now()
def pytest_runtest_makereport(item, call):
"""API测试报告增强"""
if call.when == "call" and "api" in item.keywords:
duration = (datetime.now() - item.start_time).total_seconds()
call.result.duration = duration
if hasattr(call, "request") and call.request:
call.result.request = {
"method": call.request.method,
"url": call.request.url,
"headers": dict(call.request.headers),
"body": call.request.body
}
call.result.response = {
"status_code": call.response.status_code,
"headers": dict(call.response.headers),
"body": call.response.text
}
def pytest_terminal_summary(terminalreporter):
"""API性能统计"""
api_tests = [r for r in terminalreporter.stats.get('passed', [])
if hasattr(r, 'duration')]
if api_tests:
avg_duration = sum(r.duration for r in api_tests) / len(api_tests)
terminalreporter.section("API性能统计")
terminalreporter.line(f"平均响应时间: {avg_duration:.3f}秒")
terminalreporter.line(f"最慢的3个API:")
for test in sorted(api_tests, key=lambda x: x.duration, reverse=True)[:3]:
terminalreporter.line(f" {test.nodeid}: {test.duration:.3f}秒")
实现功能:
- 自定义API基础URL
- 全局会话管理
- API请求耗时统计
- 请求/响应记录
- API性能报告
七、钩子函数执行顺序验证
可以通过以下代码验证关键钩子的执行顺序:
# conftest.py
def pytest_configure(config):
print("\n[pytest_configure] 配置初始化")
def pytest_sessionstart(session):
print("\n[pytest_sessionstart] 会话开始")
def pytest_collection_modifyitems(items):
print("\n[pytest_collection_modifyitems] 收集到{}个测试".format(len(items)))
def pytest_runtest_protocol(item, nextitem):
print(f"\n[pytest_runtest_protocol] 开始执行 {item.nodeid}")
def pytest_runtest_setup(item):
print(f"[pytest_runtest_setup] 设置 {item.nodeid}")
def pytest_runtest_teardown(item, nextitem):
print(f"[pytest_runtest_teardown] 清理 {item.nodeid}")
def pytest_sessionfinish(session, exitstatus):
print("\n[pytest_sessionfinish] 会话结束,状态码:", exitstatus)
def pytest_unconfigure(config):
print("\n[pytest_unconfigure] 配置清理")
执行测试时将看到类似输出:
[pytest_configure] 配置初始化
[pytest_sessionstart] 会话开始
[pytest_collection_modifyitems] 收集到5个测试
[pytest_runtest_protocol] 开始执行 test_sample.py::test_one
[pytest_runtest_setup] 设置 test_sample.py::test_one
[pytest_runtest_teardown] 清理 test_sample.py::test_one
[pytest_sessionfinish] 会话结束,状态码: 0
[pytest_unconfigure] 配置清理
通过合理组合这些钩子函数,可以构建强大的测试框架扩展功能,满足各种自动化测试需求。