Pytest插件运行时部分钩子函数未触发问题求助
开发Pytest插件监听测试运行,实现了多个钩子函数,但运行后仅pytest_sessionstart/pytest_sessionfinish、pytest_runtest_logstart/pytest_runtest_logfinish有输出,pytest_runtest_setup、pytest_runtest_call等钩子完全没触发。尝试改为hookwrapper模式、静态方法均无效。
环境与代码
插件代码
""" to test scripted pytest control """ import sys import pytest class MyPytestPlugin: """ Hooks to get info from pytest """ def __init__(self) -> None: pass # pylint: disable=unused-argument def pytest_collection_modifyitems(self, config, items): print("collection done.") def pytest_sessionstart(self, session): print("*" * 10) print("session started", session) print("*" * 10) def pytest_sessionfinish(self, session, exitstatus): print("-" * 10) print("session finished", session) def pytest_runtest_logstart(self, nodeid, location): print("<<<<<< log start >>>>>>>", nodeid) def pytest_runtest_logfinish(self, nodeid, location): print("<<<<<< log finish >>>>>>>", nodeid) def pytest_runtest_protocol(self, item, nextitem): print("<<<<<< protocol >>>>>>>") def pytest_collectreport(self, report): print("<<<<<< collect report >>>>>>>") def pytest_report_teststatus(self, report): print("<<<<<< test status >>>>>>>") def pytest_runtest_setup(self, item): print("<<<<<< setup >>>>>>>") def pytest_runtest_call(self, item): print("<<<<<< runtest call >>>>>>>") def pytest_runtest_teardown(self, item): print("<<<<<< runtest teardown >>>>>>>") my_plugin = MyPytestPlugin() sys.exit( pytest.main( ["-s", "sample_test.py"], plugins=[my_plugin], ) )
测试文件sample_test.py
import pytest @pytest.mark.parametrize( "a,b,expected", [ pytest.param(1, 1, 2, id="t1"), pytest.param(2, 2, 4, id="t2"), pytest.param(2, 1, 3, id="t3"), pytest.param(2, 3, 5, id="t4"), pytest.param(3, 4, 7, id="t5"), ], ) def test_addon(a, b, expected): print(f"{a}, {b}, {expected}") assert a + b == expected
运行输出(关键片段)
********** session started <Session xxxxxxxx exitstatus=<ExitCode.OK: 0> testsfailed=0 testscollected=0> ********** [gw0] Python 3.9.6 (default, Aug 17 2021, 02:38:04) -- [GCC 8.3.0] [gw1] Python 3.9.6 (default, Aug 17 2021, 02:38:04) -- [GCC 8.3.0] [gw2] Python 3.9.6 (default, Aug 17 2021, 02:38:04) -- [GCC 8.3.0] gw0 [5] / gw1 [5] / gw2 [5] scheduling tests via LoadScheduling <<<<<< log start >>>>>>> sample_test.py::test_addon[t1] # ... 仅log和session相关输出,无setup/call等日志
根因分析
问题出在xdist并行测试插件的工作机制:
- 主进程仅负责测试用例收集、任务分发、session生命周期管理,不会执行具体测试
- 实际测试用例的执行(包括setup/call/teardown等阶段)都在worker进程(即输出中的gw0/gw1/gw2)中完成
- 当前插件仅在主进程中注册,worker进程启动时没有加载这个插件,因此测试执行相关的钩子无法在worker中触发;而log类钩子是主进程负责接收worker的日志事件,所以能触发。
解决方案
方案1:临时禁用xdist(调试用)
如果不需要并行测试,直接在运行时禁用xdist插件,确保所有钩子在单进程中执行:
修改pytest.main的参数,添加-p no:xdist:
sys.exit( pytest.main( ["-s", "sample_test.py", "-p", "no:xdist"], plugins=[my_plugin], ) )
运行后所有钩子都会正常触发。
方案2:让worker进程加载插件(保留并行)
要在并行模式下触发所有钩子,必须让每个worker进程都加载你的插件,有几种实现方式:
方式A:将插件代码放到conftest.py
把MyPytestPlugin的定义放到当前目录的conftest.py中,pytest(包括worker进程)会自动加载conftest中的插件:
# conftest.py import pytest class MyPytestPlugin: # 钩子函数和之前完全一致 def pytest_sessionstart(self, session): print("*" * 10) print("session started", session) print("*" * 10) def pytest_runtest_setup(self, item): print(f"<<<<<< setup >>>>>>> [{item.nodeid}]") # ... 其他钩子函数 def pytest_configure(config): config.pluginmanager.register(MyPytestPlugin())
然后修改原运行脚本,去掉手动传入plugins的参数:
# sample_test_runner.py import sys import pytest sys.exit( pytest.main( ["-s", "sample_test.py"], ) )
这样worker进程启动时会加载conftest中的插件,所有钩子都会在对应的进程中触发(注意worker的输出会带有[gw0]等前缀)。
方式B:将插件打包为可安装包
把插件做成标准的pytest插件包(符合pytest插件规范),安装后worker进程会自动发现并加载。
方式C:通过配置文件指定插件
在tox.ini或pytest.ini中添加:
[pytest] pytest_plugins = your_plugin_module_name
确保你的插件模块能被Python导入,worker进程启动时会读取配置并加载插件。
验证
使用方案2的方式A后,运行输出会包含worker进程中的钩子日志:
[gw0] <<<<<< setup >>>>>>> [sample_test.py::test_addon[t2]] [gw0] <<<<<< runtest call >>>>>>> [gw0] 2, 2, 4 [gw0] <<<<<< runtest teardown >>>>>>>
内容的提问来源于stack exchange,提问作者Reza

