官方文档:
API Reference - pytest documentation
hook函数,pytest框架预留的函数,在测试执行的生命周期内不同阶段会调用,分为引导钩子、初始化钩子、用例收集钩子、用例执行钩子、测试报告钩子、调试钩子。
介绍几个常用的hook函数:
测试用例收集结束后调用,可用于调整测试用例的顺序。
# conftest.py
from typing import List
from _pytest.config import Config
from _pytest.main import Session
from _pytest.nodes import Item
def pytest_collection_modifyitems(session: Session, config: Config, items: List[Item]) ->None:
print('\n用例收集默认顺序:', items)
for item in items:
print('用例的nodeid:', item.nodeid, '\n用例名称:', item.name)
items.sort(key=lambda x: x.name)
print('改变后的用例顺序:', items)
# test_add_xxx.py
def test_add_b_9():
"""
这是测试用例b
"""
print(' 1 + 3 加法计算')
assert 1 + 3 == 4
def test_add_a_19():
"""
这里测试用例a
"""
print(' 10 + 13 加法计算')
assert 10 + 13 == 14
pytest测试用例,模块内的默认执行顺序是从上到下,我们给改为按用例名排序执行。
collecting ...
用例收集默认顺序: [, ]
用例的nodeid: cases/test_add_100.py::test_add_b_9
用例名称: test_add_b_9
用例的nodeid: cases/test_add_100.py::test_add_a_19
用例名称: test_add_a_19
改变后的用例顺序: [, ]
collected 2 items
不同测试阶段,生成测试报告。
# conftest.py
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> Optional[TestReport]:
"""
测试执行过程中获取不同阶段的测试报告
:param item:
:param call:
:return:
"""
out = yield
# print('用例执行结果:', out)
report = out.get_result()
print('\n', report.when, '阶段的测试报告是:', report)
if report.when == 'setup' or report.when == 'call':
print('测试阶段:', report.when)
print('测试用例的nodeid:', report.nodeid, item.nodeid)
print('测试用例描述:', item.function.__doc__)
print('测试结果:', report.outcome)
print('测试过程中的异常:', call.excinfo)
print('测试过程中的异常的详细日志:', report.longrepr)
print('用例耗时:', report.duration)
# test_add_xxx.py
def setup_module():
print('用例前置操作')
@pytest.fixture
def pre():
print('b用例的前置操作')
def test_add_b_9(pre):
"""
这是测试用例b
:param pre:
:return:
"""
print(' 1 + 3 加法计算')
assert 1 + 3 == 4
def test_add_a_19():
"""
这里测试用例a
:return:
"""
print(' 10 + 13 加法计算')
assert 10 + 13 == 14
运行命令:pytest -vs --tb=line -k add (以下同运行命令)
这个结果太长,就用截图了,中间省略了一部分结果,可自己复制代码调试。需要关注下红框里的内容,report.when、call.excinfo、report.outcome比较常用。
从运行结果发现即便不实现setup和teardown方法,也会有setup和teardown报告。如果setup定义且失败了,我们知道这时候用例和teardown步骤是不会运行的,但是teardown报告却有,call报告没有(dont know why?)。
此外,还可通过res.sections获取用例里面print的内容;
@pytest.hookimpl(hookwrapper=True) 如果hook函数中使用了yield关键字,这个装饰器必须加上,否则报错,现在还不清楚是什么原因。。。
自定义生成一份总结报告。
# conftest.py
def pytest_terminal_summary(terminalreporter: TerminalReporter, exitstatus: ExitCode, config: Config) ->None:
"""
收集测试结果
"""
result = terminalreporter.stats
print(result)
print('用例总数:', terminalreporter._numcollected)
print('用例开始的时间戳:', terminalreporter._sessionstarttime)
print('用例通过数量:', len(terminalreporter.stats.get('passed', [])))
print('用例失败数量:',len(terminalreporter.stats.get('failed', [])))
print('用例跳过数量:',len(terminalreporter.stats.get('skipped', [])))
print('用例错误数量:',len(terminalreporter.stats.get('error', [])))
# 当teardown执行失败时,用例会算通过,但同时也会计入错误个数,所以需要特殊处理下:
# print('用例通过数量:', len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown']))
# test_add_xxx.py
def test_add_b_9():
"""
这是测试用例b
"""
print(' 1 + 3 加法计算')
assert 1 + 3 == 4
def test_add_a_19():
"""
这里测试用例a
"""
print(' 10 + 13 加法计算')
assert 10 + 13 == 14
@pytest.mark.skip
def test_add_c_29():
"""
这里测试用例c
:return:
"""
print(' 20 + 23 加法计算')
assert 20 + 23 == 14
{'': [, , , , ], 'passed': [], 'failed': [], 'skipped': []}
用例总数: 3
用例开始的时间戳: -----------
用例通过数量: 1
用例失败数量: 1
用例跳过数量: 1
用例错误数量: 0
获取时间戳,可以在结束后使用“time.time()-开始时间戳”得到执行时间。
hook函数定义内容:
# conftest.py
def pytest_report_teststatus(report, config) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
"""
用于处理用例执行的状态 显示
:param report:
:param config:
:return: 第一个str暂时不知道代表什么,第二个str代表短显示(不指定-v运行时的结果显示),第三个str代表长显示(指定-v运行时的结果显示))
"""
if report.skipped:
# 如果用例跳过,不论是setup、call还是teardown
return (report.outcome, report.head_line + ' skipped\n', report.head_line + ' SKIPPED ')
if report.failed:
# 如果用例失败,不论是setup、call还是teardown
return (report.outcome, report.head_line + ' failed\n', report.head_line + ' FAILED X')
if report.passed and report.when == 'call':
# 如果用例成功,只打印call的结果
return (report.outcome, report.head_line + ' passed\n', report.head_line + ' PASSED √')
pytest -vs -k add --tb=line,用例执行结果截图:
hook函数定义内容:
# conftest.py
def pytest_generate_tests(metafunc: Metafunc):
"""
测试用例参数化收集前调用此钩子函数, 生成(多个)对测试函数的参数化调用
(实际使用场景,可参考pytest-repeat插件,利用此hook设置重复执行)
其它用法:
metafunc.config.getoption('addopts') --获取命令行传入的值
argvalues 可以通过 metafunc.module.xxx获取,xxx是参数的变量名
"""
# 如果测试用例的fixture中有param 这里是会获取所有测试用例的fixturenames
if 'param' in metafunc.fixturenames:
# 如果argvalues是空列表,则用例执行会skip
metafunc.parametrize('param', [1, 2], ids=['first', 'second'], scope='function')
测试用例:
# test_xxx.py
def test_add_1(param):
"""
这是测试用例1
:param pre:
:return:
"""
print('参数化的参数', param)
print(' 1 + 3 加法计算')
assert 1 + 3 == 4
def test_add_2(param):
"""
这里测试用例2
:return:
"""
print(' 10 + 13 加法计算')
assert 10 + 13 == 14
@pytest.mark.skip
def test_add_3():
"""
这里测试用例3
:return:
"""
print(' 20 + 23 加法计算')
assert 20 + 23 == 14
运行结果:
cases/test_add.py::test_add_1[first] PASSED [1/5]
cases/test_add.py::test_add_1[second] PASSED [2/5]
cases/test_add.py::test_add_2[first] FAILED [3/5]
cases/test_add.py::test_add_2[second] FAILED [4/5]
cases/test_add.py::test_add_3 SKIPPED (unconditional skip)
pytest_collect_file 自定义想要收集的测试用例格式,如以test开头,.yaml结束的文件作为测试用例;
pytest_ignore_collect 自定义忽略收集的测试用例,如忽略test_add.py文件;
其它hook放在一起举例说明,后续有时间再详细更新:
def pytest_addoption(parser):
print('==========这里钩子函数pytest_addoption中的打印===========')
parser.addoption('--happy', action='store_true', help='开心的定义')
def pytest_cmdline_main(config):
print('=============这里钩子函数pytest_cmdline_main中的打印==========')
print('pytest_cmdline_main中尝试获取pytest_addoption添加的参数:', config.option.happy)
def pytest_configure(config: Config):
print('=============这里钩子函数pytest_configure中的打印==============')
print('pytest_configure中尝试获取pytest_addoption添加的参数:', config.option.happy)
print('pytest_configure中尝试获取pytestconfig插件:', config.pluginmanager.get_plugin('pytestconfig'))
print('pytest_configure中中尝试获取terminalreporter插件:', config.pluginmanager.getplugin('terminalreporter'))
def pytest_sessionstart(session):
print('=============这里钩子函数pytest_sessionstart中的打印==============')
print('sssion 开始时间:',time())
print('pytest_sessionstart中尝试获取terminalreporter插件:', session.config.pluginmanager.getplugin('terminalreporter'))
def pytest_runtest_protocol(item, nextitem):
print(f"-----------------------------\n开始测试协议: {item.name}")
if nextitem is None:
print("这是最后一个测试")
def pytest_runtest_logstart(nodeid):
print(f"runtest_logstart测试开始: {nodeid}")
print('runtest_logstart开始时间:', time())
def pytest_runtest_setup(item):
print(f"setup测试: {item.name}")
# 这里可以执行一些测试前的设置工作
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_call(item):
# 这里可以执行一些测试调用前后的操作
print(f"runtest_call调用测试: {item.name}")
# # 假设我们在这里监控性能
start_call_time = time()
yield
end_call_time = time()
call_duration = end_call_time - start_call_time
print(f"runtest_call测试调用用时: {call_duration:.2f}秒")
def pytest_runtest_teardown(item, nextitem):
print(f"teardown测试: {item.name}")
# 这里可以执行一些测试后的清理工作
def pytest_runtest_logreport(report):
# if report.when == "call":
# if report.failed:
# print(f"测试失败: {report.nodeid}")
# elif report.passed:
# print(f"测试成功: {report.nodeid}")
if report.passed:
print(f"logreport测试用例 {report.nodeid} 执行成功")
elif report.failed:
print(f"logreport测试用例 {report.nodeid} 执行失败,错误信息:{report.longrepr}")
def pytest_runtest_logfinish(nodeid):
print(f"测试结束: {nodeid}")
print('runtest_logfinish结束时间:', time())
def pytest_sessionfinish(session, exitstatus):
print('=============这里钩子函数pytest_sessionfinish中的打印==============')
print('用例退出状态:', exitstatus)
def pytest_unconfigure(config: Config):
print('================这里钩子函数pytest_unconfigure中的打印===================')
pytest_addoption 注册一些命令行参数,定义可参考python设置命令行选项 ;
pytest_cmdline_main 负责执行主命令行动作。具体来说,这个钩子函数在命令行参数解析之后、测试会话开始之前被调用;以上示例中可以获取命令行参数的值;
pytest_configure / pytest_unconfigure 初始配置和结束测试后的配置,可用于插件注册或取消插件;从运行结果看,这个hook调用时,已经能获取到pytestconfig插件,但是还不能获取到terminalreporterreporter插件;(通过pytest --trace-config命令查看内置插件)
pytest_sessionstart 在创建Session对象之后、执行收集测试用例之前调用;
pytest_sessionfinish 在整个测试运行完成后调用,exitstatus表示用例退出状态,若用例全部执行成功返回0 ;
pytest_runtest_protocol 》pytest_runtest_logstart 》pytest_runtest_setup 》pytest_runtest_logreport 》pytest_runtest_call 》pytest_runtest_logreport 》pytest_runtest_teardown 》 pytest_runtest_logreport 》pytest_runtest_logfinish
对单个测试用例执行 runtest 协议,以上是hook函数的调用顺序。
import pytest
def test_demo_1(tear):
print('test_demo_1')
assert 1 == 1
@pytest.fixture
def tear():
yield
assert 1 == 2
def test_demo_2():
print('test_demo_2')
assert 2 == 2
执行命令:pytest -k demo -s --tb=line
==========这里钩子函数pytest_addoption中的打印===========
=============这里钩子函数pytest_cmdline_main中的打印==========
pytest_cmdline_main中尝试获取pytest_addoption添加的参数: False
=============这里钩子函数pytest_configure中的打印==============
pytest_configure中尝试获取pytest_addoption添加的参数: False
pytest_configure中尝试获取pytestconfig插件: <_pytest.config.Config object at 0x7f37b5ed7fb0>
pytest_configure中中尝试获取terminalreporter插件: None
=============这里钩子函数pytest_sessionstart中的打印==============
sssion 开始时间: -------
pytest_sessionstart中尝试获取terminalreporter插件: <_pytest.terminal.TerminalReporter object at 0x7f37b5c3a750>
=============================================================================================================== test session starts ================================================================================================================
xxxx
-----------------------------
开始测试协议: test_demo_1
cases/test_demo.py::test_demo_1 runtest_logstart测试开始: cases/test_demo.py::test_demo_1
runtest_logstart开始时间: --------
setup测试: test_demo_1
logreport测试用例 cases/test_demo.py::test_demo_1 执行成功
runtest_call调用测试: test_demo_1
test_demo_1
runtest_call测试调用用时: 0.00秒
PASSEDlogreport测试用例 cases/test_demo.py::test_demo_1 执行成功
teardown测试: test_demo_1
cases/test_demo.py::test_demo_1 ERRORlogreport测试用例 cases/test_demo.py::test_demo_1 执行失败,错误信息:E assert 1 == 2
测试结束: cases/test_demo.py::test_demo_1
runtest_logfinish结束时间: --------
-----------------------------
开始测试协议: test_demo_2
这是最后一个测试
xxxxxxxxxx
runtest_logfinish结束时间: --------
=============这里钩子函数pytest_sessionfinish中的打印==============
用例退出状态: 1
=================== 2 passed, 3 deselected, 1 error in 0.02s =====================================================================================================
================这里钩子函数pytest_unconfigure中的打印===================
建议自己copy代码,运行一下。运行结果太长,上述输出省略了部分内容(xxxxxxx)
pytest框架是比较庞大,了解越多越发现只是冰山一角,以上也是我参考各类相关帖子总结的内容,与各位共勉,如有不对若能指出,感激不尽~
hasattr(config, 'workerinput') ,workinput属性似乎是pytest-xdist插件提供的。并行处理和单进程执行存在一些区别,故在某些位置判断存在workinput属性就直接return了。
pytest文档35-Hooks函数之统计测试结果(pytest_terminal_summary) - 上海-悠悠 - 博客园