20.1 钩子方法的验证和执行
pytest从所有注册的插件中调用符合钩子规范的方法。我们看一个典型的钩子,作用是在所有的测试被收集之后调用:pytest_collection_modifyitems(session, config, items) 。
当我们在我们的插件中调用了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的插件能保持长期兼容性的原因。
20.2 firstresult: 在得到第一个非None的结果后停止
大多数对于pytest钩子的调用都会得到一个“结果列表”,里面包含了所有被调用钩子的非None的结果。
有一些钩子专门使用 firstresult=True 选项,这样钩子只会执行到第一个注册的函数返回一个非None的结果,这个结果也会作为整个钩子调用的结果。在这种情况下其他的钩子函数不会被调用。
20.3 hookwrapper:包裹其他钩子运行
pytest的插件可以实现一个hookwrapper,它可以包裹其他钩子运行。hookwrapper是一个只yield一次的生成器函数。当pytest加载钩子的时候,先回执行hookwrapper的代码,传递的参数也是一样的。
在yield的时候,pytest会执行之后的钩子函数,并且把结果用result实例的形式返回到yield点,这个result实例是一个封装的结果或者一个封装的异常信息。这样,yield点本身就不会抛出异常了(除非是个BUG)。
下面是一个hookwrapper的定义:
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
注意:hookwrapper不会返回结果,它仅仅是进行跟踪或者对于钩子的作用进行扩展。如果底层钩子的结果是一个可变对象,他们可能会修改这个结果,但最好避免它。
更多信息可以查看 pluggy documentation about hookwrappers.
20.4 钩子的顺序/调用的例子
对于一个给定的钩子,会有好多个实现,我们通常认为钩子是一个 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 作为hookwrapper,yield之前的代码被最先调用
- Plugin1 的 pytest_collection_modifyitems 因为标记了 tryfirst=True在之后被调用
- Plugin2的 pytest_collection_modifyitems因为标记了 trylast=True 在之后被调用(即使没有这个标签,它还是在这个位置被调用)
- Plugin3 的 pytest_collection_modifyitems yield之后的代码被最后执行。yield接收到了一个封装了钩子运行结果的result对象。wrapper不应该修改这个结果。
在我们想影响hookwrappers的执行顺序的时候,我们也可以将tryfirst 和 trylast 与hookwrapper=True 结合起来使用。
20.5 声明一个新的钩子
注意:这一节简单的介绍一下如何添加一个钩子以及他们是如何工作的,更多详细信息可以查看 the pluggy documentation.
插件和conftest文件都有可能会定义新的钩子,这些钩子可以在别的插件中被应用来修改插件的行为或者交互:
pytest_addhooks(pluginmanager: PytestPluginManager) → None
在插件注册的时候调用,允许通过pluginmanager.add_hookspecs(module_or_class, prefix).来添加新的钩子。
Parameters pluginmanager (_pytest.config.PytestPluginManager) – pytest plugin manager.
注意:这样定义的钩子与 hookwrapper=True.定义的完全不兼容
钩子函数通常是没有实现的,只包含文档字符串描述,指出了如果钩子什么时候被调用,还有如果被调用的话会返回什么样子的值。函数的名称必须是pytest_开头,否则pytest就不认识它。
下面是一个例子,我们假定这段代码在 sample_hook.py 模块中。
def pytest_my_hook(config):
"""
Receives the pytest config and does things with it
"""
要注册成为钩子的函数必须写在模块或者类中。这个模块或者类可以被传入pluginmanager 的 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)
真是的例子看,xdist里面的newhooks.py
钩子可能被夹具或者其他的钩子调用,在这两种情况下,钩子被钩子对象调用的时候都有一个config对象,大多数钩子都能够直接接收一个config对象,在使用夹具的情况下,可以使用pytestconfig夹具来提供同样的config对象。
@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)
注意:钩子在接收参数的时候必须指定key,必须是命名参数
现在你的钩子已经准备好被使用了,要注册这个钩子的监听函数,只需要在他们的conftest文件中定义一个pytest_my_hook函数,签名必须正确,例如:
def pytest_my_hook(config):
"""
Print all active hooks to the screen.
"""
print(config.hook)
20.6 在pytest_addoption中使用钩子
有时候,我们需要修改基于其他插件中定义的钩子的插件的命令行选项。例如,一个插件需要暴露一个命令行选项,而另一个插件要为它填入默认值,我们可以使用pluinmanager可以被用来安装和使用一个钩子来完成这个事情。插件可以定义和添加一个钩子,然后像下面这么使用 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 hook
pluginmanager.add_hookspecs(hook)
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,
)
在conftest中使用myplugin也像下面这样简单:
def pytest_config_file_default_value():
return "config.yaml"
20.7 从第三方插件中选择性的使用钩子
由于标准的验证规则,上面介绍的在插件中使用钩子是比较难懂的:如果你依赖了一个用户没有安装的插件,验证就会失败,但错误信息对于你的客户来说是没有多大意义的。
一种方法是把之前直接定义在插件模块中的钩子延迟到一个新的插件中,例如:
# 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())
这样做的一个额外的好处是允许你根据已经安装的插件选择性的安装钩子。