Pytest 源码解读 [1] - [pluggy] 核心设计理念浅读
背景:
Pytest 是一个功能强大的 Python 测试框架,它使用了一个名为 "pluggy" 的插件系统来扩展其功能。在 Pytest 的源码中,pluggy 模块负责实现插件管理和扩展机制。
核心类介绍:
PluginManager
类:PluginManager
是 pluggy 模块提供的一个类,用于管理插件的加载、注册和调用。它负责协调插件之间的交互,并控制钩子函数的执行顺序。
HookspecMarker
类:HookspecMarker
是 pluggy 模块提供的一个装饰器类,用于定义钩子函数规范。通过使用 HookspecMarker
装饰器,我们可以标识出一个函数作为钩子函数规范,以便在后续的插件中进行实现。
HookimplMarker
类:HookimplMarker
是 pluggy 模块提供的一个装饰器类,用于定义钩子函数的具体实现。通过使用 HookimplMarker
装饰器,我们可以将一个函数标识为钩子函数的具体实现,并将其注册到插件管理器中。
核心逻辑代码:
pm = PluginManager("pluggy_demo_1") pm.add_hookspecs(HookSpec) pm.register(HookImpl1()) pm.register(HookImpl2()) print(pm.hook.calculate(a=1, b=2))
首先,我们创建一个插件管理器 pm
,并指定项目名称为 "pluggy_demo_1"。然后,我们使用 add_hookspecs()
方法将钩子函数规范 HookSpec
添加到插件管理器中。这样,插件管理器就知道了我们定义的钩子函数规范。
接下来,我们注册两个插件 HookImpl1()
和 HookImpl2()
到插件管理器中,通过调用 register()
方法。这样,插件管理器就知道了需要执行这两个插件的钩子函数。
最后,我们通过 pm.hook.calculate()
调用了钩子函数 calculate
。pm.hook
是插件管理器提供的一个特殊属性,它允许我们访问所有已注册的钩子函数。通过调用 pm.hook.calculate()
,插件管理器会触发所有已注册的 calculate
钩子函数,并将参数 a=1
和 b=2
传递给它们。
在插件中,我们可以根据具体需求实现 calculate
钩子函数的逻辑。可以有多个插件实现了该钩子函数,每个插件的实现可以根据自己的逻辑进行计算,并返回计算结果。
总结起来,这段代码中的 pm.hook.calculate
调用了已注册的 calculate
钩子函数,并触发了相应的插件逻辑。pm.hook
是插件管理器提供的特殊属性,用于访问已注册的钩子函数。通过调用该属性上的钩子函数,可以实现插件的扩展和自定义行为。
常用的 Pytest 钩子函数:
pytest_configure(config)
: 在 Pytest 运行之前,用于配置和初始化测试运行环境。pytest_collection_modifyitems(config, items)
: 在测试收集过程中修改测试项(test items)的钩子函数。pytest_runtest_protocol(item, nextitem)
: 在执行单个测试项之前和之后,以及在测试项之间进行干预的钩子函数。pytest_pyfunc_call(pyfuncitem)
: 在执行测试函数之前和之后进行干预的钩子函数。pytest_terminal_summary(terminalreporter)
: 在测试运行完成后,用于生成测试结果摘要的钩子函数。
自定义钩子函数步骤
步骤1:定义钩子函数接口(Hook specification)
- 创建一个 Python 模块,例如
myhooks.py
。 - 在该模块中,使用
@pytest.hookspec
装饰器来标记你的钩子函数接口。例如:
# myhooks.py import pytest @pytest.hookspec def my_custom_hook(arg1, arg2): """Documentation for the custom hook.""" pass
在这个例子中,my_custom_hook
是你的自定义钩子函数接口。你可以根据需要定义参数和返回值,以及提供相应的文档说明。
步骤2:实现钩子函数(Hook implementation)
- 创建另一个 Python 模块,例如
myplugin.py
。 - 在该模块中,使用
@pytest.hookimpl
装饰器来标记你的钩子函数实现。例如:
# myplugin.py import pytest @pytest.hookimpl def my_custom_hook(arg1, arg2): """Implementation of the custom hook.""" # 执行自定义的逻辑 print(f"Running custom hook with arguments: {arg1}, {arg2}") return 42
在这个例子中,my_custom_hook
是你的自定义钩子函数实现。你可以在函数体内编写你的自定义逻辑,并返回相应的结果。
步骤3:注册自定义插件
- 创建一个用于注册插件的 Python 模块,例如
conftest.py
。这个文件通常放置在测试目录的根目录或者tests
目录下。 - 在
conftest.py
中,使用pytest_configure
钩子函数来注册你的插件。例如:
# conftest.py def pytest_configure(config): config.pluginmanager.register(myplugin)
在这个例子中,myplugin
是你在 myplugin.py
中定义的插件。通过调用 register
方法,你可以将插件注册到 Pytest 的插件管理器中。
步骤4:使用自定义钩子函数
- 在测试代码中,你可以通过使用
pytestconfig
对象来访问自定义的钩子函数。 - 调用自定义钩子函数,并传入相应的参数。例如:
# test_myhooks.py def test_with_custom_hook(pytestconfig): hook_result = pytestconfig.hook.my_custom_hook(arg1=1, arg2=2) # 处理钩子函数返回的结果 assert hook_result == 42
在这个例子中,我们通过 pytestconfig.hook
访问了自定义的钩子函数,并传入了参数 arg1
和 arg2
。你可以根据你的钩子函数逻辑来处理返回的结果。
需要注意的是,为了让 Pytest 能够识别和加载自定义的钩子函数,确保将 myhooks.py
、myplugin.py
和 conftest.py
放置在测试的搜索路径下,例如项目根目录或者 tests
目录下。