编写钩子函数¶
钩子函数的验证和执行¶
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 插件可以实现钩子包装器,它们包装其他钩子实现的执行。钩子包装器是一个生成器函数,它只 yield 一次。当 pytest 调用钩子时,它首先执行钩子包装器并传递与常规钩子相同的参数。
在钩子包装器的 yield 点,pytest 将执行下一个钩子实现并将它们的结果返回到 yield 点,或者如果它们引发异常,则会传播异常。
这是一个钩子包装器的示例定义
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
...
以下是执行顺序
Plugin3 的 pytest_collection_modifyitems 被调用直到 yield 点,因为它是一个钩子包装器。
Plugin1 的 pytest_collection_modifyitems 被调用,因为它被标记为
tryfirst=True。Plugin2 的 pytest_collection_modifyitems 被调用,因为它被标记为
trylast=True(但即使没有这个标记,它也会在 Plugin1 之后)。Plugin3 的 pytest_collection_modifyitems 然后执行 yield 点之后的代码。yield 接收调用非包装器的结果,或者如果非包装器引发异常则引发异常。
tryfirst 和 trylast 也可以用于钩子包装器,在这种情况下,它将影响钩子包装器之间的排序。
声明新钩子¶
注意
这是一个关于如何添加新钩子以及它们通常如何工作的快速概述,但更完整的概述可以在 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)
有关真实世界的示例,请参阅 xdist 中的 newhooks.py。
钩子可以从 fixture 或其他钩子调用。在这两种情况下,钩子都通过 hook 对象调用,该对象在 config 对象中可用。大多数钩子直接接收 config 对象,而 fixture 可以使用提供相同对象的 pytestconfig fixture。
@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_generate_tests 钩子在测试模块或测试类中定义时也会被发现。其他钩子必须存在于 conftest.py 插件 或外部插件中。请参阅 如何参数化 fixture 和测试函数 和 钩子。
在 pytest_addoption 中使用钩子¶
有时,有必要根据另一个插件中的钩子来更改一个插件定义命令行选项的方式。例如,一个插件可能会暴露一个命令行选项,而另一个插件需要定义其默认值。可以使用 pluginmanager 来安装和使用钩子来完成此操作。该插件将定义并添加钩子,并使用 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!"