pytest 插件-编写插件

2022-03-21 16:16 更新

为您自己的项目实现本地 ​conftest插件或可在许多项目(包括第三方项目)中使用的 pip 可安装插件很容易。

一个插件包含一个或多个钩子函数。pytest 通过调用以下插件的指定钩子来实现配置、收集、运行和报告的各个方面:

  • 内置插件:从 pytest 的内部 ​_pytest​ 目录加载。
  • 外部插件:通过 ​setuptools入口点发现的模块
  • conftest.py 插件:在测试目录中自动发现的模块

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

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

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

  1. 通过扫描命令行中的 ​-p no:name​ 选项并阻止加载该插件(即使是内置插件也可以通过这种方式阻止)。 这发生在正常的命令行解析之前。
  2. 通过加载所有内置插件。
  3. 通过扫描命令行以查找 ​-p name​ 选项并加载指定的插件。 这发生在正常的命令行解析之前。
  4. 通过加载通过 ​setuptools入口点注册的所有插件。
  5. 通过加载通过 ​PYTEST_PLUGINS环境变量指定的所有插件。
  6. 通过加载命令行调用推断的所有 ​conftest.py​ 文件:
    • 如果没有指定测试路径,则使用当前目录作为测试路径

    • 如果存在,则加载 ​conftest.py​ 和​ test*/conftest.py ​相对于第一个测试路径的目录部分。加载 ​conftest.py​ 文件后,加载其 ​pytest_plugins​ 变量中指定的所有插件(如果存在)。
    • 请注意,pytest 在工具启动时不会在更深的嵌套子目录中找到 ​conftest.py​ 文件。 将 ​conftest.py​ 文件保存在顶层测试或项目根目录中通常是个好主意。
  7. 通过递归加载 ​conftest.py​ 文件中 ​pytest_plugins​ 变量指定的所有插件。

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​”可能会产生歧义,因为在你的​PYTHONPATH​或​sys.path​中也可能有其他​conftest.py​文件。 因此,项目将 ​conftest.py​ 放在包范围内或从不从 ​conftest.py​ 文件中导入任何内容是一种很好的做法。

由于pytest在启动过程中发现插件的方式,一些钩子应该只在​plugins或位于​tests​根目录下的​conftest.py​文件中实现。

编写自己的插件

如果你想写一个插件,你可以复制很多现实生活中的例子:

  • 一个自定义集合示例插件
  • 提供 pytest 自己的功能的内置插件
  • 许多提供附加功能的外部插件

所有这些插件都实现了钩子and/or ​fixture​来扩展和添加功能。

确保查看优秀的 ​cookiecutter-pytest-plugin​ 项目,这是一个用于创作插件的 ​cookiecutter模板。

该模板提供了一个很好的起点,其中包含一个工作插件、使用 ​tox运行的测试、一个全面的 ​README文件以及一个预配置的入口点。

也考虑将你的插件贡献给 ​pytest-dev​ 一旦它有一些满意的用户而不是你自己。

让其他人可以安装您的插件

如果你想让你的插件在外部可用,你可以为你的发行版定义一个所谓的入口点,以便 pytest 找到你的插件模块。 pytest 查找 ​pytest11入口点以发现其插件,因此您可以通过在 ​setuptools-invocation​ 中定义它来使您的插件可用:

# 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​ 作为可以定义钩子的插件。

确保在您的 PyPI 分类器列表中包含 ​Framework :: Pytest​,以便用户轻松找到您的插件。

断言重写

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

如果您在需要启用断言重写的其他模块中有断言助手,则需要在导入之前明确要求 pytest 重写此模块。

register_assert_rewrite(*names)

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

此函数将确保此模块或包内的所有模块将重写其断言语句。 因此,您应该确保在实际导入模块之前调用它,如果您是使用包的插件,通常在您的 ​__init__.py​ 中。

  • raises​:​TypeError ​– 如果给定的模块名称不是字符串。
  • 参数:​names (str)
  • 返回类型:​None

当您编写使用包创建的 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​模块中标记它以便重写,当包中的模块被导入时,​__init__.py​模块总是首先被导入的。这样​plugin.py​仍然可以正常导入​helper.py​。​pytest_foo/__init__.py​的内容将需要看起来像这样:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

在测试模块或 conftest 文件中Requiring/Loading插件

你可以使用​pytest_plugins​在测试模块或​conftest.py​文件中​require​插件:

pytest_plugins = ["name1", "name2"]

当​test​模块或​conftest​插件被加载时,指定的插件也会被加载。任何模块都可以作为插件,包括应用程序的内部模块:

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins是递归处理的,所以注意上面的例子中如果​myapp.testsupport.myplugin​ 也声明了​pytest_plugins​,那么变量的内容也会被加载为插件,以此类推。

不推荐使用在非根 ​conftest.py​ 文件中使用 ​pytest_plugins​ 变量的插件。

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

这种机制使得在应用程序甚至外部应用程序中共享​fixture​变得很容易,而不需要使用​setuptools​的入口点技术创建外部插件。

pytest_plugins导入的插件也将自动标记为断言重写。 但是,要使该模块生效,必须先不导入该模块; 如果在处理 ​pytest_plugins语句时它已经被导入,则会产生警告,并且插件内的断言将不会被重写。 要解决此问题,您可以在导入模块之前自己调用 ​pytest.register_assert_rewrite()​ ,或者您可以安排代码延迟导入,直到插件注册后。

通过名称访问另一个插件

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

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

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

注册自定义标记

如果您的插件使用任何标记,您应该注册它们,以便它们出现在 pytest 的帮助文本中并且不会引起虚假警告。 例如,以下插件将为所有用户注册 ​cool_marker和 ​mark_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"]

或者,您可以使用​-p pyteste​r命令行选项调用pytest。

这将允许您使用​pytester fixture​来测试您的插件代码。

让我们用一个例子来演示你可以用这个插件做什么。假设我们开发了一个插件,它提供一个​fixture hello​,该​fixture​生成一个函数,我们可以用一个可选参数调用这个函数。它将返回一个字符串值​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​文件和测试文件提供了一个方便的API。它还允许我们运行测试并返回一个结果对象,通过这个对象我们可以断言测试的结果。

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)

此外,在运行 pytest 之前,可以将示例复制到 ​pytester的隔离环境中。 这样我们可以将测试的逻辑抽象到单独的文件中,这对于更长的测试和/或更长的 ​conftest.py​ 文件特别有用。

请注意,要使 ​pytester.copy_example​ 正常工作,我们需要在 ​pytest.ini​ 中设置 ​pytester_example_dir​ 以告诉 pytest 在哪里查找示例文件。

# 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-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
collected 2 items

test_example.py ..                                                   [100%]

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


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号