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() 调用了钩子函数 calculatepm.hook 是插件管理器提供的一个特殊属性,它允许我们访问所有已注册的钩子函数。通过调用 pm.hook.calculate(),插件管理器会触发所有已注册的 calculate 钩子函数,并将参数 a=1b=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)

  1. 创建一个 Python 模块,例如 myhooks.py
  2. 在该模块中,使用 @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)

  1. 创建另一个 Python 模块,例如 myplugin.py
  2. 在该模块中,使用 @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:注册自定义插件

  1. 创建一个用于注册插件的 Python 模块,例如 conftest.py。这个文件通常放置在测试目录的根目录或者 tests 目录下。
  2. conftest.py 中,使用 pytest_configure 钩子函数来注册你的插件。例如:
复制代码
# conftest.py
def pytest_configure(config):
    config.pluginmanager.register(myplugin)

在这个例子中,myplugin 是你在 myplugin.py 中定义的插件。通过调用 register 方法,你可以将插件注册到 Pytest 的插件管理器中。

步骤4:使用自定义钩子函数

  1. 在测试代码中,你可以通过使用 pytestconfig 对象来访问自定义的钩子函数。
  2. 调用自定义钩子函数,并传入相应的参数。例如:
复制代码
# 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 访问了自定义的钩子函数,并传入了参数 arg1arg2。你可以根据你的钩子函数逻辑来处理返回的结果。

需要注意的是,为了让 Pytest 能够识别和加载自定义的钩子函数,确保将 myhooks.pymyplugin.pyconftest.py 放置在测试的搜索路径下,例如项目根目录或者 tests 目录下。