编写插件

很容易实现 local conftest plugins 为你自己的项目或 pip-installable plugins 它可以在许多项目中使用,包括第三方项目。请参考 安装和使用插件 如果你只想使用而不想写插件。

插件包含一个或多个挂钩函数。 Writing hooks 解释如何自己编写一个钩子函数的基础和细节。 pytest 通过调用实现配置、收集、运行和报告的所有方面 well specified hooks 以下插件中的一个:

原则上,每个钩子调用都是一个 1:N python函数调用位置 N 是给定规范的已注册实现函数数。所有规范和实现都遵循 pytest_ 前缀命名约定,便于区分和查找。

工具启动时的插件发现顺序

pytest 在工具启动时按以下方式加载插件模块:

  1. 通过扫描命令行 -p no:name 期权和 舞台调度 该插件不会被加载(甚至内置插件也可以通过这种方式被阻止)。这发生在正常的命令行解析之前。

  2. 通过加载所有内置插件。

  3. 通过扫描命令行 -p name 选项并加载指定的插件。这发生在正常的命令行解析之前。

  4. 通过加载通过 setuptools entry points .

  5. 通过加载所有通过 PYTEST_PLUGINS 环境变量。

  6. 通过加载所有 conftest.py 通过命令行调用推断的文件:

    • 如果没有指定测试路径,请使用current dir作为测试路径

    • 如果存在,负载 conftest.pytest*/conftest.py 相对于第一个测试路径的目录部分。在 conftest.py 文件已加载,加载在其 pytest_plugins 变量(如果存在)。

    注意,pytest没有找到 conftest.py 工具启动时更深嵌套的子目录中的文件。通常保持你的 conftest.py 文件位于顶层测试或项目根目录中。

  7. 通过递归加载 pytest_plugins 变量在 conftest.py 文件夹。

conftest.py:本地每目录插件

局部的 conftest.py 插件包含特定于目录的钩子实现。钩子会话和测试运行活动将调用中定义的所有钩子 conftest.py 靠近文件系统根目录的文件。实施的示例 pytest_runtest_setup 钩住,以便在 a 子目录,但不适用于其他目录:

a/conftest.py:
    def pytest_runtest_setup(item):
        # called for running each test in 'a' directory
        print("setting up", item)

a/test_sub.py:
    def test_sub():
        pass

test_flat.py:
    def test_flat():
        pass

以下是您运行它的方法:

pytest test_flat.py --capture=no  # will not show "setting up"
pytest a/test_sub.py --capture=no  # will show "setting up"

注解

如果你有 conftest.py 不在python包目录中的文件(即包含 __init__.py )那么“import conftest”可能不明确,因为可能还有其他 conftest.py 文件以及您的 PYTHONPATHsys.path . 因此,项目的最佳实践是 conftest.py 在包范围内或从不从 conftest.py 文件。

参见: Pytest导入机制和 sys.path/PYTHONPATH .

注解

由于pytest如何在启动期间发现插件,一些钩子应该只在位于test根目录的插件或conftest.py文件中实现,有关详细信息,请参阅每个钩子的文档。

编写自己的插件

如果你想写一个插件,有很多真实的例子你可以从:

所有这些插件实现 hooks 和/或 fixtures 扩展和添加功能。

注解

一定要检查优秀的 cookiecutter-pytest-plugin 项目,它是 cookiecutter template 用于编写插件。

该模板提供了一个良好的起点,包括一个工作插件、使用tox运行的测试、一个全面的自述文件以及一个预先配置的入口点。

也考虑 contributing your plugin to pytest-dev 一旦它拥有了一些快乐的用户而不是你自己。

使您的插件可由其他人安装

如果您想让您的插件在外部可用,您可以为您的发行版定义一个所谓的入口点,以便 pytest 查找插件模块。入口点是由 setuptools . Pytest查找 pytest11 通过入口点发现插件,您可以通过在SETUPTOOLS调用中定义插件使其可用:

# sample ./setup.py file
from setuptools import setup

setup(
    name="myproject",
    packages=["myproject"],
    # the following makes a plugin available to pytest
    entry_points={"pytest11": ["name_of_plugin = myproject.pluginmodule"]},
    # custom PyPI classifier for pytest plugins
    classifiers=["Framework :: Pytest"],
)

如果以这种方式安装软件包, pytest 意志负荷 myproject.pluginmodule 作为一个可以定义 hooks .

注解

确保包括 Framework :: Pytest 在你的清单中 PyPI classifiers 为了方便用户找到你的插件。

断言重写

的主要特征之一 pytest 是使用普通的断言语句,并在断言失败时对表达式进行详细的自省。这是由“断言重写”提供的,它在被编译为字节码之前修改被解析的AST。这是通过一个 PEP 302 进口钩子,安装时间早 pytest 启动并在导入模块时执行此重写。但是,由于我们不想测试与生产中运行的字节码不同的字节码,所以这个钩子只重写测试模块本身(由 python_files 配置选项),以及作为插件一部分的任何模块。任何其他导入的模块都不会被重写,并且会发生正常的断言行为。

如果您在其他模块中有断言帮助程序,您需要在这些模块中启用断言重写,那么您需要询问 pytest 在导入此模块之前显式重写它。

register_assert_rewrite(*names: str)None[源代码]

注册导入时要重写的一个或多个模块名。

此函数将确保此模块或包中的所有模块将重写其断言语句。因此,在实际导入模块之前,通常在 __init__. 如果你是一个使用软件包的插件。

引发

TypeError -- 如果给定的模块名不是字符串。

这在编写使用包创建的pytest插件时尤其重要。进口钩只处理 conftest.py 文件和列在 pytest11 入口点作为插件。例如,考虑以下包:

pytest_foo/__init__.py
pytest_foo/plugin.py
pytest_foo/helper.py

以下是典型的 setup.py 提取物:

setup(..., entry_points={"pytest11": ["foo = pytest_foo.plugin"]}, ...)

仅在这种情况下 pytest_foo/plugin.py 将被重写。如果helper模块还包含需要重写的assert语句,则在导入之前需要将其标记为assert语句。最简单的方法是在 __init__.py 模块,当导入包中的模块时,始终首先导入该模块。这种方式 plugin.py 仍然可以导入 helper.py 通常情况下。内容 pytest_foo/__init__.py 然后需要看起来像这样:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

在测试模块或conftest文件中要求/加载插件

您可以在测试模块或 conftest.py 文件使用 pytest_plugins

pytest_plugins = ["name1", "name2"]

当测试模块或ConfTest插件被加载时,指定的插件也将被加载。任何模块都可以作为插件使用,包括内部应用程序模块:

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins 递归处理,因此请注意在上面的示例中如果 myapp.testsupport.myplugin 也宣布 pytest_plugins ,变量的内容也将作为插件加载,等等。

注解

需要使用插件 pytest_plugins 非根变量 conftest.py 文件已弃用。

这很重要,因为 conftest.py 文件实现每个目录挂钩实现,但一旦导入插件,它将影响整个目录树。为了避免混淆,定义 pytest_plugins 在任何 conftest.py 不推荐使用不在tests根目录中的文件,它将引发警告。

这种机制使在应用程序甚至外部应用程序中共享设备变得容易,而无需使用 setuptools 的切入点技术。

插件导入者 pytest_plugins 也将自动标记为断言重写(请参见 pytest.register_assert_rewrite() )但是,要使此操作生效,不能已经导入模块;如果在 pytest_plugins 语句被处理后,将产生一个警告,插件内的断言将不会被重写。要解决这个问题,您可以拨打 pytest.register_assert_rewrite() 在导入模块之前,您可以自己安排代码延迟导入,直到插件注册之后。

按名称访问另一个插件

如果一个插件想要与另一个插件的代码协作,它可以通过插件管理器获取一个引用,如下所示:

plugin = config.pluginmanager.get_plugin("name_of_plugin")

如果要查看现有插件的名称,请使用 --trace-config 选择权。

注册自定义标记

如果您的插件使用了任何标记,您应该注册它们,以便它们出现在pytest的帮助文本中,而不是 cause spurious warnings . 例如,以下插件将注册 cool_markermark_with 对于所有用户:

def pytest_configure(config):
    config.addinivalue_line("markers", "cool_marker: this one is for cool tests.")
    config.addinivalue_line(
        "markers", "mark_with(arg, arg2): this marker takes arguments."
    )

测试插件

pytest附带一个名为 pytester 这有助于为插件代码编写测试。该插件在默认情况下是禁用的,因此您必须先启用它,然后才能使用它。

您可以通过将以下行添加到 conftest.py 测试目录中的文件:

# content of conftest.py

pytest_plugins = ["pytester"]

或者,可以使用调用pytest -p pytester 命令行选项。

这将允许您使用 pytester 用于测试插件代码的夹具。

让我们用一个例子来演示如何使用插件。假设我们开发了一个插件来提供一个夹具 hello 它产生一个函数,我们可以用一个可选参数来调用这个函数。它将返回字符串值 Hello World! 如果我们不提供价值或 Hello {{value}}! 如果我们提供一个字符串值。

import pytest


def pytest_addoption(parser):
    group = parser.getgroup("helloworld")
    group.addoption(
        "--name",
        action="store",
        dest="name",
        default="World",
        help='Default "name" for hello().',
    )


@pytest.fixture
def hello(request):
    name = request.config.getoption("name")

    def _hello(name=None):
        if not name:
            name = request.config.getoption("name")
        return "Hello {name}!".format(name=name)

    return _hello

现在 pytester fixture为创建临时 conftest.py 文件和测试文件。它还允许我们运行测试并返回一个结果对象,通过该对象我们可以断言测试的结果。

def test_hello(pytester):
    """Make sure that our plugin works."""

    # create a temporary conftest.py file
    pytester.makeconftest(
        """
        import pytest

        @pytest.fixture(params=[
            "Brianna",
            "Andreas",
            "Floris",
        ])
        def name(request):
            return request.param
    """
    )

    # create a temporary pytest test file
    pytester.makepyfile(
        """
        def test_hello_default(hello):
            assert hello() == "Hello World!"

        def test_hello_name(hello, name):
            assert hello(name) == "Hello {0}!".format(name)
    """
    )

    # run all tests with pytest
    result = pytester.runpytest()

    # check that all 4 tests passed
    result.assert_outcomes(passed=4)

在PyIt上运行一个示例之前,可以在PyIt上复制一个示例。

# content of pytest.ini
[pytest]
pytester_example_dir = .
# content of test_example.py


def test_plugin(pytester):
    pytester.copy_example("test_example.py")
    pytester.runpytest("-k", "test_example")


def test_example():
    pass
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR, configfile: pytest.ini
collected 2 items

test_example.py ..                                                   [100%]

============================ 2 passed in 0.12s =============================

有关结果对象的详细信息, runpytest() 返回,以及它提供的方法请检查 RunResult 文档。

编写钩子函数

钩子功能验证和执行

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 为了“未来兼容”:我们可以引入新的hook命名参数,而不破坏现有hook实现的签名。这是Pytest插件长期通用兼容性的原因之一。

注意钩子的功能不同于 pytest_runtest_* 不允许引发异常。这样做将中断Pytest运行。

第一个结果:在第一个非无结果处停止

大多数调用 pytest 钩子导致 结果清单 它包含被调用的钩子函数的所有非空结果。

一些钩子规格使用 firstresult=True 选项,以便钩子调用只执行到n个已注册函数中的第一个返回一个非none结果,然后将该结果作为整个钩子调用的结果。在这种情况下,不会调用其余的钩子函数。

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

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

在钩子包装器的屈服点上,pytest将执行下一个钩子实现,并以 Result 封装结果或异常信息的实例。因此,屈服点本身通常不会引发异常(除非存在错误)。

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

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

注意,钩子包装器本身并不返回结果,它们只是围绕实际钩子实现执行跟踪或其他副作用。如果底层钩子的结果是一个可变的对象,那么它们可以修改该结果,但最好避免这样做。

有关详细信息,请参阅 pluggy documentation about hookwrappers .

钩子函数排序/调用示例

对于任何给定的钩子规范,可能有多个实现,因此我们通常认为 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(hookwrapper=True)
def pytest_collection_modifyitems(items):
    # will execute even before the tryfirst one above!
    outcome = yield
    # will execute after all non-hookwrappers executed

执行顺序如下:

  1. Plugin3的pytest_collection_modifyitems一直调用到屈服点,因为它是一个钩子包装器。

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

  3. 之所以调用Plugin2的pytest_collection_modifyitems,是因为它被标记为 trylast=True (但即使没有这个标记,它也会出现在plugin1之后)。

  4. Plugin3的pytest_collection_modifyitems,然后在屈服点之后执行代码。收益率得到 Result 封装调用非包装器的结果的实例。包装机不得修改结果。

可以使用 tryfirsttrylast 也与 hookwrapper=True 在这种情况下,它会相互影响卷纸器的顺序。

声明新挂钩

注解

这是关于如何添加新钩子以及它们一般如何工作的简要概述,但是可以在中找到更完整的概述 the pluggy documentation .

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

pytest_addhooks(pluginmanager: PytestPluginManager)None[源代码]

在插件注册时调用,以允许通过调用 pluginmanager.add_hookspecs(module_or_class, prefix) .

参数

pluginmanager (_pytest.config.PytestPluginManager) -- pytest插件管理器。

注解

这个钩子与 hookwrapper=True .

钩子通常声明为不做任何事情的函数,这些函数只包含描述何时调用钩子以及预期返回值的文档。函数名必须以开头 pytest_ 否则Pytest无法识别它们。

这是一个例子。假设此代码在 sample_hook.py 模块。

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

要用pytest注册钩子,它们需要在自己的模块或类中进行结构化。然后可以将该类或模块传递给 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)

有关真实世界的示例,请参见 newhooks.pyxdist .

钩子可以从固定装置或其他钩子中调用。在这两种情况下,钩子都是通过 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)

注解

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

现在你的钩子可以用了。要在钩子上注册函数,其他插件或用户现在必须简单地定义函数 pytest_my_hook 在他们的 conftest.py .

例子:

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

在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 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.py使用myplugin将简单地定义钩子,如下所示:

def pytest_config_file_default_value():
    return "config.yaml"

可以选择使用第三方插件中的挂钩

使用插件中的新钩子(如上所述)可能有点困难,因为标准 validation mechanism :如果您依赖未安装的插件,则验证将失败,并且错误消息对您的用户没有多大意义。

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

# 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())

这有一个额外的好处,即允许您根据安装的插件有条件地安装钩子。