pytest8.x版本 中文使用文档-------17.编写插件

目录

钩子函数验证与执行

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

钩子包装器:围绕其他钩子执行

钩子函数排序/调用示例

声明新钩子

在pytest_addoption中使用钩子

可选地使用第三方插件的钩子

跨钩子函数在items上存储数据


钩子函数验证与执行

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

  • session:当前的 pytest 测试会话对象,包含了许多关于测试会话的信息,比如配置选项、收集的测试项等。
  • config:pytest 的配置对象,提供了访问配置文件和命令行选项的接口。
  • items:一个列表,包含了所有已收集的测试项。每个测试项都代表了一个待执行的测试函数或方法。

通过这个钩子,插件可以执行各种操作,比如过滤掉不满足特定条件的测试项,或者根据测试项的属性重新排序测试项等。编写 pytest_collection_modifyitems 钩子函数时,你需要确保你的插件已经正确注册,并且该函数位于一个 pytest 可以识别并加载的模块中。然后,在适当的时机(比如测试集合完成后),pytest 会自动调用这个函数,并传入必要的参数。

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

让我们看一个可能的实现:

def pytest_collection_modifyitems(config, items):  
    # 在收集完成后被调用  
    # 你可以修改 `items` 列表  
    ...

在这里,pytest 会传入 config(pytest 配置对象)和 items(收集到的测试项列表),但不会传入 session 参数,因为我们在函数签名中没有列出它。这种参数的“动态裁剪”允许 pytest 保持“未来兼容性”:我们可以在不破坏现有钩子实现签名的情况下引入新的钩子命名参数。这是 pytest 插件通常能够长期保持兼容性的原因之一。

请注意,除了以 pytest_runtest_* 开头的钩子函数外,其他钩子函数不允许抛出异常。如果这样做,将会中断 pytest 的运行。

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

大多数对 pytest 钩子的调用会生成一个结果列表,该列表包含了所有被调用的钩子函数返回的非None结果。

一些钩子规范使用 firstresult=True 选项,这样钩子调用就只会执行到N个已注册函数中的第一个返回非None结果为止,该结果随后被视为整个钩子调用的结果。在这种情况下,剩余的钩子函数将不会被调用。

钩子包装器:围绕其他钩子执行

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

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

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

import pytest  
  
@pytest.hookimpl(wrapper=True)  
def pytest_pyfunc_call(pyfuncitem):  
    # 在下一个钩子执行之前执行一些操作  
    do_something_before_next_hook_executes()  
  
    # 这里使用 yield 语句来暂停当前函数的执行,并允许 pytest 执行下一个钩子函数  
    # 如果下一个钩子函数执行时抛出了异常,那么这个异常将会被传播并在这里重新抛出  
    res = yield  
  
    # 在下一个钩子执行之后,对结果进行后处理  
    new_res = post_process_result(res)  
  
    # 覆盖并返回给插件系统的新结果  
    # 注意:这里返回的值将作为 pytest_pyfunc_call 钩子的结果  
    return new_res

在这个例子中,pytest_pyfunc_call 是一个钩子包装器,它通过 @pytest.hookimpl(wrapper=True) 装饰器标记为包装器。这个包装器会在 pytest 调用 pytest_pyfunc_call 钩子之前和之后执行自定义代码。

需要注意的是,钩子包装器必须返回一个结果给钩子,或者抛出一个异常。这是因为 pytest 期望每个钩子(包括包装器)都有一个明确的返回值或异常,以便它可以正确地处理测试流程。

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

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

如果钩子实现因异常而失败,包装器可以在 yield 周围使用 try-catch-finally 结构来处理该异常,可以选择传播它、抑制它或完全抛出一个不同的异常。

有关更多信息,请参阅 pluggy 文档中关于钩子包装器的部分 pluggy documentation about hook wrappers

钩子函数排序/调用示例

对于任何给定的钩子规范,可能存在多个实现,因此我们通常将钩子执行视为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
        ...

在这个例子中,tryfirst=True 和 trylast=True 参数分别用于确保某个钩子实现尽可能早或尽可能晚地执行。然而,需要注意的是,使用 wrapper=True 的钩子包装器具有特殊的执行顺序。包装器会在所有其他非包装器钩子之前执行其 yield 之前的代码,并在所有非包装器钩子之后执行其 yield 之后的代码(如果有的话,如 finally 块)。这意味着包装器可以在其他钩子之前和之后执行自定义逻辑,但它不会改变其他非包装器钩子之间的相对执行顺序。

执行顺序如下:

  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 会接收非包装器调用的结果,或者在非包装器抛出异常时抛出异常。

在钩子包装器上使用 tryfirst 和 trylast 也是可能的,这将影响包装器之间的执行顺序。

声明新钩子

注意

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

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

pytest_addhooks(pluginmanager)                     [source]

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

参数:

请注意此钩子与钩子包装器不兼容。

在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暴露的一个钩子)将这个类或模块传递给插件管理器。

def pytest_addhooks(pluginmanager):  
    """这个例子假设钩子被组织在'sample_hook'模块中。"""  
    from my_app.tests import sample_hook  
  
    pluginmanager.add_hookspecs(sample_hook)

对于实际的应用示例,请参见 xdist中的 newhooks.py文件。

钩子既可以从fixture中调用,也可以从其他钩子中调用。在这两种情况下,钩子都是通过配置对象中的钩子对象来调用的。大多数钩子直接接收一个配置对象,而fixture则可以使用pytestconfig fixture,它提供了相同的对象。

@pytest.fixture()  
def my_fixture(pytestconfig):  
    # 调用名为"pytest_my_hook"的钩子  
    # 'result'将是所有已注册函数返回值的列表。  
    result = pytestconfig.hook.pytest_my_hook(config=pytestconfig)

注意:钩子只使用关键字参数来接收参数。

现在你的钩子已经准备好被使用了。为了将函数注册到钩子上,其他插件或用户现在只需在他们的conftest.py文件中定义具有正确签名的pytest_my_hook函数即可。

def pytest_my_hook(config):  
    """  
    将所有活动的钩子打印到屏幕上。  
    """  
    print(config.hook)

pytest_addoption中使用钩子

有时,基于另一个插件中的钩子来改变一个插件定义命令行选项的方式是必要的。例如,一个插件可能暴露一个命令行选项,而另一个插件需要为这个选项定义默认值。这时,可以使用插件管理器来安装和使用钩子来实现这一点。插件将定义并添加钩子,并按如下方式使用pytest_addoption

# hooks.py 的内容  
  
# 使用 firstresult=True 因为我们只想让一个插件定义这个默认值  
@hookspec(firstresult=True)  
def pytest_config_file_default_value():  
    """返回配置文件命令行选项的默认值。"""  
  
# myplugin.py 的内容  
  
def pytest_addhooks(pluginmanager):  
    """这个例子假设钩子被组织在 'hooks' 模块中。"""  
    from . import hooks  
  
    pluginmanager.add_hookspecs(hooks)  
  
def pytest_addoption(parser, pluginmanager):  
    # 从钩子中获取配置文件的默认值  
    default_value = pluginmanager.hook.pytest_config_file_default_value()  
    # 向命令行参数解析器添加 --config-file 选项  
    # 其中包括帮助信息和默认值  
    parser.addoption(  
        "--config-file",  
        help="要使用的配置文件,默认为 %(default)s",  
        default=default_value,  
    )

在这个例子中,pytest_config_file_default_value 钩子被定义为一个 hookspec,并且指定了 firstresult=True,这意味着当多个插件尝试定义这个钩子时,pytest 将只使用第一个返回的结果。然后,在 myplugin.py 中,这个钩子被添加到插件管理器中,并在 pytest_addoption 函数中被调用以获取配置文件的默认值。最后,这个默认值被用于 parser.addoption 方法中,以定义 --config-file 命令行选项的默认值。

使用mypluginconftest.py文件将简单地按以下方式定义钩子:

def pytest_config_file_default_value():  
    return "config.yaml"

pytest_config_file_default_value函数作为pytest_config_file_default_value钩子的一个实现被定义。当pytest运行并遇到需要这个钩子的地方(比如myplugin插件在pytest_addoption函数中调用它时),它会查找所有已注册的钩子实现,并由于这个钩子被标记为firstresult=True(在hooks.py中定义时),pytest将只使用找到的第一个实现(在这个例子中是conftest.py中定义的函数)。这个函数返回"config.yaml"作为配置文件的默认值。

可选地使用第三方插件的钩子

如上所述,使用插件中的新钩子可能会因为标准的验证机制而稍显复杂:如果你依赖的插件没有被安装,验证将失败,并且错误消息对于你的用户来说可能难以理解。

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

# myplugin.py 的内容  
  
class DeferPlugin:  
    """简单插件,用于推迟 pytest-xdist 的钩子函数。"""  
  
    def pytest_testnodedown(self, node, error):  
        """标准的 xdist 钩子函数。"""  
  
def pytest_configure(config):  
    if config.pluginmanager.hasplugin("xdist"):  
        config.pluginmanager.register(DeferPlugin())

这种方法的一个额外好处是,它允许你根据已安装的插件来条件性地安装钩子。在这个例子中,pytest_configure 钩子函数在 pytest 配置阶段被调用,它检查 xdist 插件是否已经被安装。如果是,它就注册一个包含 pytest_testnodedown 钩子函数的新插件实例(DeferPlugin)。这样,只有当 xdist 插件可用时,pytest_testnodedown 钩子才会被安装和使用。这种方法提高了插件的灵活性和兼容性。

跨钩子函数在items上存储数据

插件经常需要在一个钩子实现中在项(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!"

“stash”在所有节点类型(如ClassSession)上都是可用的,并且如果需要的话,在Config上也是可用的。这种方式避免了直接修改项对象,减少了与其他插件或工具(如mypy)的冲突风险。

  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值