Pytest 源码解读 [7] - PyTest on pluggy

之前花了很多篇幅来介绍 pluggy 这个插件框架。核心原因就是因为其实 pytest 是一个完全基于 pluggy 开发的测试框架,这个也可以解释为什么说 pytest 是一个很灵活的测试框架, 支持很多插件 (https://docs.pytest.org/en/7.0.x/reference/plugin_list.html)。 其实原因就在这里,因为它的内核本来就是一个插件引擎 。

看下 pytest 的源码里面有一个 hookspec.py 的文件,这里定义了 pytest 申明的所有符合 pluggy 规范的 HookSpec ,这里例举了部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 @hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
"""Called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop.

Stops at first non-None result, see :ref:`firstresult`.

:param pytest.Config config: The pytest config object.
"""


def pytest_load_initial_conftests(
early_config: "Config", parser: "Parser", args: List[str]
) -> None:
"""Called to implement the loading of initial conftest files ahead
of command line option parsing.

.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.

:param pytest.Config early_config: The pytest config object.
:param List[str] args: Arguments passed on the command line.
:param pytest.Parser parser: To add command line options.
"""

这里定义的所有 HookSpec,在 src/_pytest 的不同模块中有对应的 HookImpl 实现,并通过 PytestPluginManager 这个继承自 pluggy - PluginManager 的核心组件,通过预先定义的一个完整测试流程,来组装不同的插件从而实现测试逻辑。

通过阅读源码来分析 PyTest 是如何通过组装这些 Hook 来实现测试流程是比较麻烦的,这里就又可以用到 pluggyenable_tracing 方式,来显示记录一次执行过程中的所有 plugin 执行情况和顺序 。方式很简单,只要在运行测试的时候,设置环境变量 PYTEST_DEBUG=1 , 就会在 consoel 打印对应的插件执行过程,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/Users/xxx/.pyenv/versions/pytesttest/bin/python /Users/xxx/Workspace/pytesttest/pytesttracer/pytest_trace.py
pytest_plugin_registered [hook]
plugin: <_pytest.config.Config object at 0x10193aac0>
manager: <_pytest.config.PytestPluginManager object at 0x1010ed880>
finish pytest_plugin_registered --> [] [hook]
pytest_addoption [hook]
parser: <_pytest.config.argparsing.Parser object at 0x10193ab80>
pluginmanager: <_pytest.config.PytestPluginManager object at 0x1010ed880>
finish pytest_addoption --> [] [hook]
pytest_addoption [hook]
parser: <_pytest.config.argparsing.Parser object at 0x10193ab80>
pluginmanager: <_pytest.config.PytestPluginManager object at 0x1010ed880>
finish pytest_addoption --> [] [hook]
pytest_plugin_registered [hook]
plugin: <module '_pytest.mark' from '/Users/markshao/.pyenv/versions/pytesttest/lib/python3.9/site-packages/_pytest/mark/__init__.py'>
manager: <_pytest.config.PytestPluginManager object at 0x1010ed880>
finish pytest_plugin_registered --> [] [hook]
....

为了方便可视化,我基于 graphviz 手撸了一个可视化的 trace 版本,并且把一些重点的 HookSpec 高亮了,后面也会给大家做进一步的实现分析。图有点大,本想弄个 FancyApp 支持 Zoom In/Out 的,可惜前端技术有限,大家可以下载到本地进行缩放查看