pytest 插件-编写钩子函数
钩子函数验证和执行
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:停止在第一个非无结果
大多数对 pytest 钩子的调用都会产生一个结果列表,其中包含被调用钩子函数的所有非无结果。
一些钩子规范使用 firstresult=True
选项,因此钩子调用只执行,直到 N
个注册函数中的第一个返回非无结果,然后将其作为整个钩子调用的结果。 在这种情况下,不会调用剩余的钩子函数。
hookwrapper:围绕其他钩子执行
Pytest插件可以实现钩子包装器来包装其他钩子实现的执行。钩子包装器是一个生成器函数,它只生成一次。当pytest调用钩子时,它首先执行钩子包装器,并传递与常规钩子相同的参数。
在钩子包装器的yield
点,pytest将执行下一个钩子实现,并将它们的结果以result
实例的形式返回给yield
点,该实例封装了一个结果或异常信息。因此,yield点本身通常不会引发异常(除非有bug)。
下面是一个钩子包装器的定义示例:
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_pyfunc_call(pyfuncitem):
do_something_before_next_hook_executes()
outcome = yield
# outcome.excinfo may be None or a (cls, val, tb) tuple
res = outcome.get_result() # will raise if outcome was exception
post_process_result(res)
outcome.force_result(new_res) # to override the return value to the plugin system
请注意,钩子包装器本身不会返回结果,它们只是围绕实际的钩子实现执行跟踪或其他副作用。 如果底层钩子的结果是一个可变对象,他们可能会修改该结果,但最好避免它。
钩子函数ordering/call的例子
对于任何给定的钩子规范,都可能有多个实现,因此我们通常将钩子的执行视为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(hookwrapper=True)
def pytest_collection_modifyitems(items):
# will execute even before the tryfirst one above!
outcome = yield
# will execute after all non-hookwrappers 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接收到一个Result实例,该实例通过调用非包装器封装了结果。包装器不得修改结果。
也可以将 tryfirst
和 trylast
与 hookwrapper=True
结合使用,在这种情况下,它会影响 hookwrapper
之间的顺序。
声明新的钩子
插件和 conftest.py
文件可以声明新的钩子,然后其他插件可以实现这些钩子,以改变行为或与新插件交互:
pytest_addhooks(pluginmanager)
在插件注册时调用以允许通过调用 pluginmanager.add_hookspecs(module_or_class, prefix)
添加新的钩子。
- 参数:
pluginmanager
(pytest.PytestPluginManager) – The pytest plugin manager. - 返回类型:
None
这个钩子与 hookwrapper=True
不兼容。
钩子通常被声明为无操作函数,其中仅包含描述何时调用钩子以及预期返回值的文档。 函数的名称必须以 pytest_
开头,否则 pytest 将无法识别它们。
这是一个例子。 假设这段代码在 sample_hook.py
模块中。
def pytest_my_hook(config):
"""
Receives the pytest config and does things with it
"""
要使用 pytest 注册钩子,它们需要在自己的模块或类中构建。 然后可以使用 pytest_addhooks
函数(它本身是 pytest 公开的钩子)将此类或模块传递给插件管理器。
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)
钩子可以从fixture
中调用,也可以从其他钩子中调用。在这两种情况下,钩子都是通过配置对象中可用的钩子对象调用的。大多数钩子直接接收配置对象,而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_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())
这有一个额外的好处,允许你根据安装的插件有条件地安装钩子。
跨钩子函数存储数据
插件通常需要在一个钩子实现中存储Items
上的数据,然后在另一个钩子实现中访问它。一个常见的解决方案是直接在项目上分配一些私有属性,但是像mypy
这样的类型检查器不赞成这样做,而且它还可能导致与其他插件的冲突。所以pytest提供了一种更好的方法,item.stash
要在插件中使用stash
,首先要在插件的顶层某处创建stash keys
:
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!"
在所有节点类型(如Class
、Session
)和Config
(如果需要的话)上都可以使用stash
。
更多建议: