...

Pytest权威教程18-插件编写

返回: Pytest权威教程

插件编写

很容易为你自己的项目实现[本地conftest插件或可以在许多项目中使用的可[安装的插件,包括第三方项目。如果你只想使用但不能编写插件,请参阅[安装和使用插件。

插件包含一个或多个钩子(hooks)方法函数。[编写钩子(hooks)方法解释了如何自己编写钩子(hooks)方法函数的基础知识和细节。pytest通过调用以下插件的[指定挂钩来实现配置,收集,运行和报告的所有方面:

  • 内置插件:从pytest的内部_pytest目录加载。
  • 外部插件:已安装的第三方插件,通过其打包元数据中的入口点发现。
  • conftest.py插件:在测试目录中自动发现的模块

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

Pytest启动时的插件发现顺序

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

  • 通过加载所有内置插件

  • 通过加载通过[setuptools入口点注册的所有插件。

  • 通过预扫描选项的命令行并在实际命令行解析之前加载指定的插件。-pname

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

    • 如果未指定测试路径,则使用当前dir作为测试路径

    • 如果存在,则加载conftest.pytest*/conftest.py相对于第一个测试路径的目录部分。

      请注意,pytestconftest.py在工具启动时没有在更深的嵌套子目录中找到文件。将conftest.py文件保存在顶级测试或项目根目录中通常是个好主意。

  • 通过递归加载文件中pytest_plugins变量指定的所有插件conftest.py

conftest.py:本地目录插件

本地conftest.py插件包含特定于目录的钩子(hooks)方法实现。Hook Session和测试运行活动将调用conftest.py靠近文件系统根目录的文件中定义的所有挂钩。实现pytest_runtest_setup钩子(hooks)方法的示例,以便在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  # 不显示 "setting up"
pytest a/test_sub.py --capture=no  # 显示 "setting up"

注意

如果你的conftest.py文件不在python包目录中(即包含一个__init__.py),那么“import conftest”可能不明确,因为conftest.pyPYTHONPATH或者也可能有其他文件sys.path。因此,项目要么放在conftest.py包范围内,要么永远不从conftest.py文件中导入任何内容,这是一种很好的做法。

另请参见: pytest import mechanisms和sys.path / PYTHONPATH。

编写自己的插件

如果你想编写插件,可以从中复制许多现实示例如:

  • 自定义集合示例插件: 在Yaml文件中指定测试的基本示例
  • 内置插件,提供pytest自己的函数
  • 许多外部插件提供额外的函数

所有这些插件都实现了钩子(hooks)方法以扩展和添加函数。

注意

请务必查看优秀的cookiecutter-pytest-plugin

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

另外考虑[将你的插件贡献给pytest-dev一旦它拥有一些非自己的快乐用户。

使你的插件可以被他人安装

如果你想让你的插件在外部可用,你可以为你的发行版定义一个所谓的入口点,以便pytest找到你的插件模块。入口点是setuptools。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"],
)

如果以这种方式安装包,pytestmyproject.pluginmodule作为可以定义钩子的插件加载。

注意: 确保包含在PyPI分类器

断言重写

其中一个主要特性pytest是使用普通的断言语句以及断言失败时表达式的详细内省。这是由“断言重写”提供的,它在编译为字节码之前修改了解析的AST。这是通过一个完成的PEP 302导入挂钩,在pytest启动时及早安装,并在导入模块时执行此重写。但是,由于我们不想测试不同的字节码,因此你将在生产中运行此挂钩仅重写测试模块本身以及作为插件一部分的任何模块。任何其他导入的模块都不会被重写,并且会发生正常的断言行为。

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

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

此函数将确保此模块或程序包内的所有模块将重写其assert语句。因此,你应确保在实际导入模块之前调用此方法,如果你是使用包的插件,则通常在__init__.py中调用。

  • 抛出: 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被重写。如果辅助模块还包含需要重写的断言语句,则需要在导入之前将其标记为这样。

通过将其标记为在__init__.py模块内部进行重写,这是最简单的,当导入包中的模块时,将始终首先导入该模块。这种方式plugin.py仍然可以helper.py正常导入。

然后,内容pytest_foo/__init__.py将需要如下所示:

import pytest

pytest.register_assert_rewrite("pytest_foo.helper")

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

你可以在测试模块或这样的conftest.py文件中要求插件:

pytest_plugins = ["name1","name2"]

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

pytest_plugins = "myapp.testsupport.myplugin"

pytest_plugins变量是递归处理的,所以请注意,在上面的示例中,如果myapp.testsupport.myplugin也声明pytest_plugins,变量的内容也将作为插件加载,依此类推。

注意

pytest_plugins不建议使用非根conftest.py文件中使用变量的插件。

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

这种机制使得在应用程序甚至外部应用程序中共享Fixture方法变得容易,而无需使用setuptools入口点技术创建外部插件。

导入的插件pytest_plugins也会自动标记为断言重写(请参阅参考资料pytest.register_assert_rewrite()在导入模块之前自行调用,也可以安排代码以延迟导入,直到注册插件为止。

按名称访问另一个插件

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

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

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

注册为通用标记

测试插件

pytest附带一个名为的插件pytester,可帮助你为插件代码编写测试。默认情况下,该插件处于禁用状态,因此你必须先启用它,然后才能使用它。

你可以通过conftest.py将以下行添加到测试目录中的文件来执行此操作:

# content of conftest.py

pytest_plugins = ["pytester"]

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

这将允许你使用testdirfixture来测试你的插件代码。

让我们用一个例子演示你可以用插件做什么。想象一下,我们开发了一个插件,它提供了一个hello产生函数的fixture,我们可以用一个可选参数调用这个函数。如果我们不提供值或者我们提供字符串值,它将返回字符串值。HelloWorld!``Hello{value}!

# -*- coding: utf-8 -*-

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

现在,testdirfixture提供了一个方便的API来创建临时conftest.py文件和测试文件。它还允许我们运行测试并返回一个结果对象,通过它我们可以断言测试的结果。

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

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

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

    # create a temporary pytest test file
    testdir.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 = testdir.runpytest()

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

另外,可以在运行pytest之前复制示例文件夹的示例

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

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

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

test_example.py ..                                                  [100%]

============================= warnings summary =============================
test_example.py::test_plugin
  $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
    testdir.copy_example("test_example.py")

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================== 2 passed,1 warnings in 0.12 seconds ===================

有关runpytest()返回的结果对象及其提供的方法的更多信息,请查看RunResult文档。

posted @ 2019-09-06 20:15  韩志超  阅读(2340)  评论(3编辑  收藏  举报