一、介绍
在编程领域,hook 函数(钩子函数) 是一种在特定事件或流程发生时被自动调用的函数,用于拦截、修改或扩展原有程序的行为。它的核心作用是 “挂钩” 到程序的特定执行点,在不修改原有代码的前提下,实现自定义逻辑。
(1)核心特点
- 触发时机固定:通常与特定事件绑定(如程序启动、函数调用前、数据处理后等)。
- 非侵入式扩展:无需修改原有代码,通过注册 hook 函数即可扩展功能。
- 灵活性高:可动态添加 / 移除,适应不同场景的需求。
(2)常见应用场景
- 框架扩展很多测试框架(如 pytest)、Web 框架(如 Django)通过 hook 函数允许用户自定义流程。例如,pytest 的pytest_runtest_makereport钩子可用于在测试用例执行后生成自定义报告。
- 事件拦截在 GUI 编程中,hook 函数可拦截鼠标点击、键盘输入等事件,实现自定义响应逻辑。
- 日志与监控在函数调用前后插入 hook 函数,记录调用参数、返回值或执行时间,用于调试或性能监控。
- 权限控制在接口请求处理前,通过 hook 函数验证用户权限,决定是否允许继续执行。
(3)pytest 中的 hook 函数示例
pytest 框架内置了大量 hook 函数,用于扩展测试流程。例如,自定义测试报告格式:
# conftest.py(pytest自动识别的钩子文件)
def pytest_runtest_makereport(item, call):
"""在测试用例执行后生成报告"""
if call.when == "call": # 当测试用例执行时
if call.excinfo is not None: # 若测试失败
print(f"用例 {item.nodeid} 执行失败!")
二、例:pytest_runtest_makereport
pytest_runtest_makereport 是 pytest 框架中一个非常重要的 钩子函数(hook function),用于在测试用例执行的不同阶段生成测试报告信息。它的核心作用是 拦截测试用例的执行过程,获取执行状态、结果和详细信息,方便开发者自定义测试报告、记录日志或执行失败后的操作(如截图)。
1. 关键参数解析
def pytest_runtest_makereport(item, call):
# ...
(1)item
测试用例对象,包含用例的元信息,如:
item.nodeid:用例的唯一标识(格式如 文件名::类名::方法名)。item.function:测试函数对象,可通过item.function.__doc__获取用例文档字符串(描述信息)。
(2)call
测试用例执行的调用对象,包含执行过程中的细节,如:
call.when:标记当前执行阶段("setup"/"call"/"teardown")。call.excinfo:执行过程中抛出的异常信息(若有),可用于获取错误堆栈。
2. 执行阶段(call.when)
测试用例的执行分为三个阶段,pytest_runtest_makereport 会在每个阶段被调用一次:
"setup":测试用例的前置操作阶段(如 fixture 的前置代码)。"call":测试用例的核心执行阶段(即测试函数本身的逻辑)。"teardown":测试用例的后置操作阶段(如 fixture 的后置代码,如driver.quit())。
通过判断 call.when,可以针对性地处理不同阶段的逻辑(例如只关注 call 阶段的失败)。
3. 返回值与 TestReport 对象
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
# 可以获取测试用例的执行结果,yield,返回一个result对象
out = yield
res = out.get_result()
函数中通过 out = yield 暂停执行,等待测试阶段完成后,out.get_result() 会返回一个 TestReport 对象。
TestReport对象 包含的核心属性:
obj.when:同call.when,标识当前阶段。obj.outcome:测试结果("passed"成功 /"failed"失败 /"skipped"跳过)。obj.nodeid:用例唯一标识(同item.nodeid)。obj.longrepr:详细的执行日志(失败时包含错误堆栈信息)。
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
# 可以获取测试用例的执行结果,yield,返回一个result对象
out = yield
"""
返回一个result对象(out)获取调用结果的测试报告,返回一个report对象
report对象的属性
包括when(setup,call,teardown三个值)、nodeid(测试用例的名字)、
outcome(用例的执行结果:passed,failed)
"""
res = out.get_result()
print("执行结果:{}".format(res))
if res.when == "call" and res.outcome == "failed": # 只获取call用例失败时的信息
print("测试用例:{}".format(item))
print("用例描述:{}".format(item.function.__doc__))
print("测试步骤:{}".format(call))
print("用例失败异常信息:{}".format(call.excinfo))
print("用例失败时的详细日志:{}".format(res.longrepr))
4. 实际应用场景
最常见的用法是 在测试用例失败时自动记录信息或截图(结合 Selenium 等工具)。下例:在自动化测试中捕获失败的测试用例并自动添加截图到 Allure 报告中。
import allure
import pytest
from selenium import webdriver
# 用例前后置
@pytest.fixture(scope="package")
def driver_fix():
# 用例前置
# 初始化浏览器对象并启动浏览器
driver = webdriver.Chrome()
# 返回浏览器对象给用例
yield driver
# 用例的后置处理:关闭浏览器
driver.quit()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
# 可以获取测试用例的执行结果,yield,返回一个result对象
out = yield
"""
返回一个result对象(out)获取调用结果的测试报告,返回一个report对象
report对象的属性
包括when(setup,call,teardown三个值)、nodeid(测试用例的名字)、
outcome(用例的执行结果:passed,failed)
"""
report = out.get_result()
# 仅仅获取用例call阶段的执行结果,不包含setup和teardown
if report.when == 'call':
# 获取用例call执行结果为结果为失败的情况
# 从测试用例的参数中获取 driver(fixture 名为 driver_fix)
driver = item.funcargs.get("driver_fix")
if driver: # 确保 driver 存在且未被关闭
"""
hasattr(对象, 属性或方法的名称)用于判断对象是否具有指定的属性或方法。
函数返回一个布尔值,如果对象具有指定的属性或方法,则返回True,否则返回False。
"""
# 检查报告对象中是否有wasxfail属性,表示测试用例是否被标记为预期失败(xfail)
xfail = hasattr(report, "wasxfail")
# 如果测试用例被跳过且是预期失败,或者测试用例执行失败且不是预期失败
if (report.skipped and xfail) or (report.failed and not xfail):
# 添加allure报告截图
with allure.step("添加失败截图"):
# 使用allure自带的添加附件的方法:三个参数分别为:源文件、文件名、文件类型
allure.attach(driver.get_screenshot_as_png(), "失败截图", allure.attachment_type.PNG)
(1)解析
钩子函数定义
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport():
out = yield
report = out.get_result()
@pytest.hookimpl(hookwrapper=True):这是 pytest 的钩子装饰器,hookwrapper=True表示这个钩子会包装原有的 pytest 行为。pytest_runtest_makereport:这是 pytest 内置的钩子函数,用于在测试用例执行的不同阶段(setup、call、teardown)生成测试报告。out = yield:暂停钩子执行,让 pytest 执行实际的测试逻辑,然后通过out.get_result()获取测试结果(即 report 对象)。
判断测试结果
xfail = hasattr(report, "wasxfail")
if (report.skipped and xfail) or (report.failed and not xfail):
# ...
xfail:判断测试是否被标记为 预期失败(通过@pytest.mark.xfail装饰器)。- 触发截图的条件:
report.skipped and xfail:测试被跳过且是预期失败(例如,预期失败的条件未满足)。report.failed and not xfail:测试实际失败且不是预期失败。
添加截图到 Allure 报告
with allure.step("添加失败截图"):
allure.attach(driver.get_screenshot_as_png(), "失败截图", allure.attachment_type.PNG)
allure.step:在 Allure 报告中创建一个步骤节点,便于结构化展示。allure.attach:将截图作为附件添加到报告中:driver.get_screenshot_as_png():调用 Selenium 的 WebDriver 获取当前页面截图(假设driver是全局变量或通过 fixture 注入)。"失败截图":附件的名称,会显示在报告中。allure.attachment_type.PNG:指定附件类型为 PNG 图片。
(2)扩展:预期失败(xfail)
使用 @pytest.mark.xfail 标记一个测试用例时,表示:
- 预期这个测试会失败(例如,测试一个尚未修复的 bug 或未实现的功能)。
- 如果测试实际失败,pytest 会将其标记为 XPASS(预期失败但实际通过,可能表示 bug 已修复)。
- 如果测试实际通过,pytest 会将其标记为 XFAIL(预期失败且实际失败,符合预期)。
wasxfail 属性的作用
- 当测试用例被标记为
xfail且实际执行结果为 失败 时,测试报告对象(report)会包含wasxfail属性,值为True。 - 若测试用例未被标记为
xfail,或标记了但实际通过(XPASS),则report对象不会有wasxfail属性。
5. 完整实践代码简单示例
conftest.py
#!/usr/bin/env python
# encoding: utf-8
import allure
import pytest
from selenium import webdriver
# 用例前后置
@pytest.fixture(scope="package")
def driver_fix():
# 用例前置
# 初始化浏览器对象并启动浏览器
driver = webdriver.Chrome()
# 返回浏览器对象给用例
yield driver
# 用例的后置处理:关闭浏览器
driver.quit()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
# 可以获取测试用例的执行结果,yield,返回一个result对象
out = yield
"""
返回一个result对象(out)获取调用结果的测试报告,返回一个report对象
report对象的属性
包括when(setup,call,teardown三个值)、nodeid(测试用例的名字)、
outcome(用例的执行结果:passed,failed)
"""
report = out.get_result()
# 仅仅获取用例call阶段的执行结果,不包含setup和teardown
if report.when == 'call':
# 获取用例call执行结果为结果为失败的情况
# 从测试用例的参数中获取 driver(fixture 名为 driver_fix)
driver = item.funcargs.get("driver_fix")
if driver: # 确保 driver 存在且未被关闭
"""
hasattr(对象, 属性或方法的名称)用于判断对象是否具有指定的属性或方法。
函数返回一个布尔值,如果对象具有指定的属性或方法,则返回True,否则返回False。
"""
# 检查报告对象中是否有wasxfail属性,表示测试用例是否被标记为预期失败(xfail)
xfail = hasattr(report, "wasxfail")
# 如果测试用例被跳过且是预期失败,或者测试用例执行失败且不是预期失败
if (report.skipped and xfail) or (report.failed and not xfail):
# 添加allure报告截图
with allure.step("添加失败截图"):
# 使用allure自带的添加附件的方法:三个参数分别为:源文件、文件名、文件类型
allure.attach(driver.get_screenshot_as_png(), "失败截图", allure.attachment_type.PNG)
test_case.py
#!/usr/bin/env python
# encoding: utf-8
'''
@Software: PyCharm
@File : test_case.py
@Time : 2023/8/29 17:01
@desc :
'''
from time import sleep
import allure
@allure.title("百度搜索")
def test_baidu(driver_fix):
"""
百度搜索测试用例
"""
driver = driver_fix
driver.get('http://www.baidu.com')
sleep(1)
driver.find_element('id', 'kw').send_keys('allure报告失败截图')
sleep(1)
driver.find_element('id', 'su').click()
sleep(1)
assert driver.title == "11allure报告失败截图_百度搜索"
sleep(3)
main_run.py
#!/usr/bin/env python
# encoding: utf-8
import os
import pytest
def run():
# pytest.main(['-sv'])
pytest.main(['-v', 'test_case.py', '--alluredir', './result', '--clean-alluredir'])
os.system('allure generate ./result/ -o ./report --clean')
if __name__ == '__main__':
run()
报告截图
三个文件在同一层目录下,运行main_run.py文件,生成的report用浏览器打开后如下图:


2394

被折叠的 条评论
为什么被折叠?



