pytest测试框架 —— 钩子函数:功能、参数、案例详解

本教程深入讲解 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="快速模式(跳过长耗时测试)")

注意事项

  1. conftest.py 或插件中定义
  2. 使用 getoption() 获取命令行参数值
  3. 使用 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")

注意事项

  1. 在多进程运行(xdist)时,每个工作进程都会调用
  2. 避免在此钩子中执行耗时操作
  3. 修改配置后需确保向后兼容

二、测试收集钩子

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

注意事项

  1. 直接修改 items 列表(不要创建新列表)
  2. 使用 item.add_marker 添加标记
  3. 使用 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()

注意事项

  1. 此钩子中抛出的任何异常都会被视为测试错误
  2. 若在此处跳过测试,测试状态为 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}")

注意事项

  1. 如果要自定义测试执行,需要在此钩子中调用 item.obj()
  2. 默认会执行测试函数,除非覆盖此钩子

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)

注意事项

  1. 即使测试失败也会调用此钩子
  2. 清理操作应幂等(可重复执行)

四、报告与日志钩子

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_addoptionparser
初始化pytest_configureconfig
收集pytest_collectionsession
收集pytest_collection_modifyitemsitems, config
执行pytest_runtest_setupitem
执行pytest_runtest_callitem
执行pytest_runtest_teardownitem, nextitem
Fixturepytest_fixture_setupfixturedef, request
Fixturepytest_fixture_post_finalizerfixturedef, request
报告pytest_report_headerconfig
报告pytest_runtest_logreportreport
报告pytest_terminal_summaryterminalreporter, exitstatus, config
异常pytest_exception_interactnode, call, report
会话pytest_sessionstartsession
会话pytest_sessionfinishsession, exitstatus
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yant224

点滴鼓励,汇成前行星光🌟

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值