pytest通过pytest_runtest_makereport添加失败截图到Allure报告中
一、介绍
在编程领域,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),用于在测试用例执行的不同阶段生成测试报告信息。它的核心作用是 拦截测试用例的执行过程,获取执行状态、结果和详细信息,方便开发者自定义测试报告、记录日志或执行失败后的操作(如截图)。
2. 关键参数解析
def pytest_runtest_makereport(item, call):# ...
(1)item
测试用例对象,包含用例的元信息,如:
item.nodeid
:用例的唯一标识(格式如 文件名::类名::方法名)。item.function
:测试函数对象,可通过item.function.__doc__
获取用例文档字符串(描述信息)。
(2)call
测试用例执行的调用对象,包含执行过程中的细节,如:
call.when
:标记当前执行阶段("setup"
/"call"
/"teardown"
)。call.excinfo
:执行过程中抛出的异常信息(若有),可用于获取错误堆栈。
3. 执行阶段(call.when)
测试用例的执行分为三个阶段,pytest_runtest_makereport
会在每个阶段被调用一次:
"setup"
:测试用例的前置操作阶段(如 fixture 的前置代码)。"call"
:测试用例的核心执行阶段(即测试函数本身的逻辑)。"teardown"
:测试用例的后置操作阶段(如 fixture 的后置代码,如driver.quit()
)。
通过判断 call.when
,可以针对性地处理不同阶段的逻辑(例如只关注 call
阶段的失败)。
4. 返回值与 TestReport 对象
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):# 可以获取测试用例的执行结果,yield,返回一个result对象out = yieldres = 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))
5. 实际应用场景
最常见的用法是 在测试用例失败时自动记录信息或截图(结合 Selenium 等工具)。下例:在自动化测试中捕获失败的测试用例并自动添加截图到 Allure 报告中。
import allure
import pytestfrom 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和teardownif 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 = yieldreport = 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
属性。
6. 完整实践代码简单示例
conftest.py
#!/usr/bin/env python
# encoding: utf-8import allure
import pytestfrom 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和teardownif 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 sleepimport allure@allure.title("百度搜索")
def test_baidu(driver_fix):"""百度搜索测试用例"""driver = driver_fixdriver.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-8import os
import pytestdef 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用浏览器打开后如下图: