编写钩子函数

钩子函数验证和执行

pytest 会针对任何给定的钩子规范从已注册的插件中调用钩子函数。我们来看看 pytest_collection_modifyitems(session, config, items) 钩子的一个典型钩子函数,pytest 会在收集所有测试项完成后调用此钩子。

当我们在插件中实现 pytest_collection_modifyitems 函数时,pytest 会在注册期间验证你使用的参数名称是否与规范匹配,如果不匹配则退出。

我们来看看一个可能的实现

def pytest_collection_modifyitems(config, items):
    # called after collection is completed
    # you can modify the ``items`` list
    ...

在此处,pytest 会传入 config(pytest 配置对象)和 items(已收集的测试项列表),但不会传入 session 参数,因为我们没有在函数签名中列出它。这种动态“修剪”参数允许 pytest 具备“向前兼容性”:我们可以在不破坏现有钩子实现签名的前提下引入新的钩子命名参数。这是 pytest 插件普遍具备长期兼容性的原因之一。

请注意,除了 pytest_runtest_* 之外的钩子函数不允许引发异常。这样做会中断 pytest 运行。

firstresult:在第一个非 None 结果处停止

pytest 钩子的大多数调用都会生成结果列表,其中包含所调用的钩子函数的所有非 None 结果。

一些钩子规范使用 firstresult=True 选项,以便钩子调用仅执行到 N 个已注册函数中的第一个返回非 None 结果时为止,然后将该结果作为整体钩子调用的结果。在这种情况下,不会调用剩余的钩子函数。

钩子包装器:在其他钩子周围执行

pytest 插件可以实现钩子包装器,这些包装器包装其他钩子实现的执行。钩子包装器是一个生成器函数,仅生成一次。当 pytest 调用钩子时,它首先执行钩子包装器,并传递与常规钩子相同的参数。

在钩子包装器的生成点,pytest 将执行下一个钩子实现,并将它们的结果返回到生成点,或者如果它们引发了异常,则传播异常。

下面是钩子包装器的示例定义

import pytest


@pytest.hookimpl(wrapper=True)
def pytest_pyfunc_call(pyfuncitem):
    do_something_before_next_hook_executes()

    # If the outcome is an exception, will raise the exception.
    res = yield

    new_res = post_process_result(res)

    # Override the return value to the plugin system.
    return new_res

钩子包装器需要为钩子返回结果,或引发异常。

在许多情况下,包装器只需要围绕实际钩子实现执行跟踪或其他副作用,在这种情况下,它可以返回 yield 的结果值。最简单(尽管无用)的钩子包装器是 return (yield)

在其他情况下,包装器希望调整或改编结果,在这种情况下,它可以返回一个新值。如果底层钩子的结果是一个可变对象,则包装器可以修改该结果,但最好避免这样做。

如果钩子实现因异常而失败,则包装器可以使用 try-catch-finally 来处理该异常,方法是在 yield 周围使用它,通过传播、抑制它或完全引发不同的异常。

有关更多信息,请参阅 关于钩子包装器的 pluggy 文档

钩子函数排序/调用示例

对于任何给定的挂钩规范,可能有多个实现,因此我们通常将 hook 执行视为 1:N 函数调用,其中 N 是已注册函数的数量。有办法影响挂钩实现是否在其他实现之前或之后,即函数 N 大小列表中的位置

# Plugin 1
@pytest.hookimpl(tryfirst=True)
def pytest_collection_modifyitems(items):
    # will execute as early as possible
    ...


# Plugin 2
@pytest.hookimpl(trylast=True)
def pytest_collection_modifyitems(items):
    # will execute as late as possible
    ...


# Plugin 3
@pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items):
    # will execute even before the tryfirst one above!
    try:
        return (yield)
    finally:
        # will execute after all non-wrappers executed
        ...

以下是执行顺序

  1. Plugin3 的 pytest_collection_modifyitems 被调用,直到 yield 点,因为它是一个挂钩包装器。

  2. Plugin1 的 pytest_collection_modifyitems 被调用,因为它标记为 tryfirst=True

  3. Plugin2 的 pytest_collection_modifyitems 被调用,因为它标记为 trylast=True(但即使没有此标记,它也会在 Plugin1 之后)。

  4. 然后,Plugin3 的 pytest_collection_modifyitems 执行 yield 点之后的代码。yield 接收从调用非包装器获得的结果,或者在非包装器引发异常时引发异常。

也可以在挂钩包装器上使用 tryfirsttrylast,在这种情况下,它将影响挂钩包装器之间的顺序。

声明新挂钩

注意

这是一个关于如何添加新挂钩以及它们如何工作的快速概述,但可以在 pluggy 文档 中找到更完整的概述。

插件和 conftest.py 文件可以声明新挂钩,然后其他插件可以实现这些挂钩,以改变行为或与新插件交互

pytest_addhooks(pluginmanager)[source]

在插件注册时调用,以允许通过调用 pluginmanager.add_hookspecs(module_or_class, prefix) 添加新挂钩。

参数:

pluginmanager (PytestPluginManager) – pytest 插件管理器。

注意

此挂钩与挂钩包装器不兼容。

在 conftest 插件中使用

如果 conftest 插件实现了此挂钩,则将在 conftest 注册时立即调用它。

挂钩通常声明为不执行任何操作的函数,其中仅包含描述何时调用挂钩以及预期返回什么的文档。函数的名称必须以 pytest_ 开头,否则 pytest 将无法识别它们。

这里有一个示例。我们假设此代码位于 sample_hook.py 模块中。

def pytest_my_hook(config):
    """
    Receives the pytest config and does things with it
    """

要使用 pytest 注册挂钩,需要将它们组织在自己的模块或类中。然后,可以使用 pytest_addhooks 函数(它本身是 pytest 公开的挂钩)将此类或模块传递给 pluginmanager

def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'sample_hook' module."""
    from my_app.tests import sample_hook

    pluginmanager.add_hookspecs(sample_hook)

有关实际示例,请参见 newhooks.py,来自 xdist

钩子可以从夹具或其他钩子中调用。在这两种情况下,钩子都是通过 hook 对象调用的,该对象在 config 对象中可用。大多数钩子直接接收 config 对象,而夹具可以使用 pytestconfig 夹具,该夹具提供相同的对象。

@pytest.fixture()
def my_fixture(pytestconfig):
    # call the hook called "pytest_my_hook"
    # 'result' will be a list of return values from all registered functions.
    result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)

注意

钩子仅使用关键字参数接收参数。

现在,您的钩子已准备好使用。要将函数注册到钩子,其他插件或用户现在只需在 conftest.py 中定义具有正确签名的函数 pytest_my_hook

示例

def pytest_my_hook(config):
    """
    Print all active hooks to the screen.
    """
    print(config.hook)

在 pytest_addoption 中使用钩子

有时,需要根据另一个插件中的钩子来更改命令行选项的定义方式。例如,一个插件可能会公开一个命令行选项,另一个插件需要为其定义默认值。插件管理器可用于安装和使用钩子来实现此目的。插件将定义并添加钩子,并按如下方式使用 pytest_addoption

# contents of hooks.py


# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)
def pytest_config_file_default_value():
    """Return the default value for the config file command line option."""


# contents of myplugin.py


def pytest_addhooks(pluginmanager):
    """This example assumes the hooks are grouped in the 'hooks' module."""
    from . import hooks

    pluginmanager.add_hookspecs(hooks)


def pytest_addoption(parser, pluginmanager):
    default_value = pluginmanager.hook.pytest_config_file_default_value()
    parser.addoption(
        "--config-file",
        help="Config file to use, defaults to %(default)s",
        default=default_value,
    )

使用 myplugin 的 conftest.py 只需按如下方式定义钩子

def pytest_config_file_default_value():
    return "config.yaml"

选择性地使用来自第三方插件的钩子

如上所述,使用插件中的新钩子可能有点棘手,因为标准 验证机制:如果您依赖未安装的插件,验证将失败,并且错误消息对您的用户来说毫无意义。

一种方法是将钩子实现推迟到新插件,而不是直接在插件模块中声明钩子函数,例如

# contents of myplugin.py


class DeferPlugin:
    """Simple plugin to defer pytest-xdist hook functions."""

    def pytest_testnodedown(self, node, error):
        """standard xdist hook function."""


def pytest_configure(config):
    if config.pluginmanager.hasplugin("xdist"):
        config.pluginmanager.register(DeferPlugin())

这具有额外的优点,即允许您根据安装的插件有条件地安装钩子。

在钩子函数中存储项目上的数据

插件通常需要在钩子实现中将数据存储在 Item 中,并在另一个钩子实现中访问它。一种常见的解决方案是直接在项目上分配一些私有属性,但是像 mypy 这样的类型检查器对此不赞成,并且它也可能与其他插件发生冲突。因此,pytest 提供了一种更好的方法来执行此操作,item.stash

要在插件中使用“stash”,首先在插件的顶层某个位置创建“stash 密钥”

been_there_key = pytest.StashKey[bool]()
done_that_key = pytest.StashKey[str]()

然后使用密钥在某个时间点隐藏您的数据

def pytest_runtest_setup(item: pytest.Item) -> None:
    item.stash[been_there_key] = True
    item.stash[done_that_key] = "no"

并在另一个时间点检索它们

def pytest_runtest_teardown(item: pytest.Item) -> None:
    if not item.stash[been_there_key]:
        print("Oh?")
    item.stash[done_that_key] = "yes!"

可以在所有节点类型(如 ClassSession)以及 Config(如果需要)上使用 stash。