一文搞懂Pytest 钩子函数

135 阅读5分钟

钩子函数运行原理

Pytest 的钩子函数机制是基于插件系统的,其核心原理如下:

  1. 插件架构:Pytest 通过 setuptools 入口点发现插件,所有钩子函数都通过插件形式实现

  2. 钩子调用点:Pytest 在测试执行的各个阶段预留了特定的调用点(hook点)

  3. 执行顺序:钩子函数按插件注册顺序执行,除非使用 tryfirst 或 trylast 标记

  4. 自动发现:Pytest 会自动发现 conftest.py 文件和已安装插件中的钩子函数

  5. 参数传递:每个钩子函数都有特定的参数签名,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}秒")

实现功能

  1. 自定义API基础URL
  2. 全局会话管理
  3. API请求耗时统计
  4. 请求/响应记录
  5. 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] 配置清理

通过合理组合这些钩子函数,可以构建强大的测试框架扩展功能,满足各种自动化测试需求。