pytest的钩子函数介绍

官方文档:

API Reference - pytest documentation

前言

hook函数,pytest框架预留的函数,在测试执行的生命周期内不同阶段会调用,分为引导钩子、初始化钩子、用例收集钩子、用例执行钩子、测试报告钩子、调试钩子。


介绍几个常用的hook函数:

一、pytest_collection_modifyitems

测试用例收集结束后调用,可用于调整测试用例的顺序。

1.conftest.py文件

# 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)

2.测试用例test_xxx.py文件

# 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

3.运行结果

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   

二、pytest_runtest_makereport

不同测试阶段,生成测试报告。

 1.conftest.py文件

# 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)

2.测试用例test_xxx.py文件

# 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

3.运行结果

运行命令:pytest -vs --tb=line -k add (以下同运行命令)

这个结果太长,就用截图了,中间省略了一部分结果,可自己复制代码调试。需要关注下红框里的内容,report.when、call.excinfo、report.outcome比较常用。

从运行结果发现即便不实现setup和teardown方法,也会有setup和teardown报告。如果setup定义且失败了,我们知道这时候用例和teardown步骤是不会运行的,但是teardown报告却有,call报告没有(dont know why?)。

pytest的钩子函数介绍_第1张图片

此外,还可通过res.sections获取用例里面print的内容;

@pytest.hookimpl(hookwrapper=True) 如果hook函数中使用了yield关键字,这个装饰器必须加上,否则报错,现在还不清楚是什么原因。。。

三、pytest_terminal_summary

自定义生成一份总结报告。

1.conftest.py文件

# 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']))

2.测试用例test_xxx.py文件

# 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

3.运行结果

{'': [, , , , ], 'passed': [], 'failed': [], 'skipped': []}
用例总数: 3
用例开始的时间戳: -----------
用例通过数量: 1
用例失败数量: 1
用例跳过数量: 1
用例错误数量: 0

获取时间戳,可以在结束后使用“time.time()-开始时间戳”得到执行时间。

四、pytest_report_teststatus

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,用例执行结果截图:

pytest的钩子函数介绍_第2张图片

五、pytest_generate_tests

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 和 pytest_ignore_collect

pytest_collect_file   自定义想要收集的测试用例格式,如以test开头,.yaml结束的文件作为测试用例;

pytest_ignore_collect 自定义忽略收集的测试用例,如忽略test_add.py文件;

七、其它hook函数

其它hook放在一起举例说明,后续有时间再详细更新:

1.conftest.py文件

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函数的调用顺序。

2.测试用例test_xxx.py文件

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

3.运行结果

执行命令: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) - 上海-悠悠 - 博客园

你可能感兴趣的:(pytest,pytest)