Pytest官方教程-19-插件编写

目录:

  1. 安装及入门
  2. 使用和调用方法
  3. 原有TestSuite使用方法
  4. 断言的编写和报告
  5. Pytest fixtures:清晰 模块化 易扩展
  6. 使用Marks标记测试用例
  7. Monkeypatching/对模块和环境进行Mock
  8. 使用tmp目录和文件
  9. 捕获stdout及stderr输出
  10. 捕获警告信息
  11. 模块及测试文件中集成doctest测试
  12. skip及xfail: 处理不能成功的测试用例
  13. Fixture方法及测试用例的参数化
  14. 缓存: 使用跨执行状态
  15. unittest.TestCase支持
  16. 运行Nose用例
  17. 经典xUnit风格的setup/teardown
  18. 安装和使用插件
  19. 插件编写
  20. 编写钩子(hook)方法
  21. 运行日志
  22. API参考
    1. 方法(Functions)
    2. 标记(Marks)
    3. 钩子(Hooks)
    4. 装置(Fixtures)
    5. 对象(Objects)
    6. 特殊变量(Special Variables)
    7. 环境变量(Environment Variables)
    8. 配置选项(Configuration Options)
  23. 优质集成实践
  24. 片状测试
  25. Pytest导入机制及sys.path/PYTHONPATH
  26. 配置选项
  27. 示例及自定义技巧
  28. Bash自动补全设置

插件编写

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

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

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

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

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

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

  • 通过加载所有内置插件

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

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

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

    • 如果未指定测试路径,则使用当前dir作为测试路径
    • 如果存在,则加载conftest.pytest*/conftest.py相对于第一个测试路径的目录部分。

    请注意,pytest conftest.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  # will not show "setting up"
pytest a/test_sub.py --capture=no  # will show "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 项目,该项目是 用于创作插件的cookiecutter模板。

该模板提供了一个很好的起点,包括一个工作插件,使用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分类器列表中, 以便用户轻松找到你的插件。Framework :: Pytest

断言重写

其中一个主要特性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不在测试根目录中的文件中进行定义 ,并将发出警告。

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

导入的插件pytest_plugins也会自动标记为断言重写(请参阅参考资料pytest.register_assert_rewrite())。但是,为了使其具有任何效果,必须不必导入模块; 如果在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 。-p pytester

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

让我们用一个例子演示你可以用插件做什么。想象一下,我们开发了一个插件,它提供了一个hello产生函数的fixture ,我们可以用一个可选参数调用这个函数。如果我们不提供值或者我们提供字符串值,它将返回字符串值。Hello World!``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文档。

编写钩子(hooks)方法函数

钩子(hooks)方法函数验证和执行

pytest为任何给定的钩子(hooks)方法规范调用已注册插件的钩子(hooks)方法函数。让我们看一下钩子(hooks)方法的典型钩子(hooks)方法函数,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“未来兼容”:我们可以引入新的钩子(hooks)方法命名参数而不破坏现有钩子(hooks)方法实现的签名。这是pytest插件的一般长期兼容性的原因之一。

请注意,除了pytest_runtest_*不允许引发异常之外的钩子(hooks)方法函数。这样做会打破pytest运行。

firstresult:首先停止非无结果

大多数对pytest钩子(hooks)方法的调用都会产生一个结果列表,其中包含被调用钩子(hooks)方法函数的所有非None结果。

一些钩子(hooks)方法规范使用该firstresult=True选项,以便钩子(hooks)方法调用仅执行,直到N个注册函数中的第一个返回非None结果,然后将其作为整个钩子(hooks)方法调用的结果。在这种情况下,不会调用其余的钩子(hooks)方法函数。

hookwrapper:在其他钩子(hooks)方法周围执行

版本2.7中的新功能。

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

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

以下是钩子(hooks)方法包装器的示例定义:

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

请注意,钩子(hooks)方法包装器本身不返回结果,它们只是围绕实际的钩子(hooks)方法实现执行跟踪或其他副作用。如果底层钩子(hooks)方法的结果是一个可变对象,它们可能会修改该结果,但最好避免它。

有关更多信息,请参阅插件文档。

钩子(hooks)方法函数排序/调用示例

对于任何给定的钩子(hooks)方法规范,可能存在多个实现,因此我们通常将hook执行视为 1:N函数调用,其中N是已注册函数的数量。有一些方法可以影响钩子(hooks)方法实现是在其他人之前还是之后,即在N-sized函数列表中的位置:

# 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被调用直到屈服点,因为它是一个钩子(hooks)方法包装器。
  2. 调用Plugin1的pytest_collection_modifyitems是因为它标有tryfirst=True
  3. 调用Plugin2的pytest_collection_modifyitems因为它被标记trylast=True(但即使没有这个标记,它也会在Plugin1之后出现)。
  4. 插件3的pytest_collection_modifyitems然后在屈服点之后执行代码。yield接收一个Result实例,该实例封装了调用非包装器的结果。包装不得修改结果。

这是可能的使用tryfirst,并trylast结合还 hookwrapper=True处于这种情况下,它会影响彼此之间hookwrappers的排序。

声明新钩子(hooks)方法

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

在插件注册时调用,允许通过调用添加新的挂钩 。pluginmanager.add_hookspecs(module_or_class, prefix)
参数: | pluginmanager_pytest.config.PytestPluginManager) - pytest插件管理器

注意:
这个钩子(hooks)方法与之不相容hookwrapper=True

钩子(hooks)方法通常被声明为do-nothing函数,它们只包含描述何时调用钩子(hooks)方法以及期望返回值的文档。

有关示例,请参阅xdist中的newhooks.py。

可选择使用第三方插件的钩子(hooks)方法

由于标准的验证机制,如上所述使用插件中的新钩子(hooks)方法可能有点棘手:如果你依赖未安装的插件,验证将失败并且错误消息对你的用户没有多大意义。

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

# contents of myplugin.py

class DeferPlugin(object):
    """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())

这具有额外的好处,允许你根据安装的插件有条件地安装挂钩。

你可能感兴趣的:(Pytest官方教程-19-插件编写)