Skip to content

Plugins

第五章:插件

尽管pytest现有功能已经十分强大,但是当添加插件之后,就会锦上添花。Pytest 代码库是通过定制和扩展来构建的,并且有一些钩子函数(hook functions)可以通过插件进行修改和改进。

如果已经学完了本书前面的章节,可能会惊讶地发现你已经写了一些插件。每当我们把夹具(fixture)或钩子函数放到项目的顶层的插件文件conftest.py中,其实就是创建了一个本地的 conftest 插件。只需要做一点额外的工作,就可以将这些conftest.py文件转换成可安装的插件,然后就可以愉快地在项目之间,与他人甚至是全世界来分享这些插件。

我们将在这一章,看看在哪里可以获取第三方插件。有相当多的插件可用,所以很有可能,有人已经把你想要对pytest做的优化想法进行了实现。因为我们将关注开源插件,如果一个插件几乎做了你想做的事情,但又差点意思,你可以fork这个项目,或把它作为创建自己的插件的一个参考。

本章是关于如创建自己的插件。顺便,在附录3会告诉你一些可行的插件。

在这一章中,我们将学习创建、测试、打包和分发插件的最佳实践。关于Python打包和发布的话题足够写一本书了,所以我们这里不会面面俱到。本章将讨论一些用最少的工作量获得PyPI 现有插件的捷径。

📌 温馨提示:在本章中执行终端命令的基准目录一律为测试目录,即ch5/?/tasks_proj/tests。如果有指定cd命令的,则需要先切换到该目录后再执行对应命令。

5.1 获取插件

我们可以在很多地方找到pytest的第三方插件。附录3中列出的插件都可以从 PyPI下载。然而,这并不是寻找优秀插件的唯一地方。

Pytest官网

https://docs.pytest.org/en/latest/plugins.html

这是pytest官方文档地址,上面有一个讨论安装和使用pytest 插件的页面,列出了一些常见的插件。

PyPI

https://pypi.python.org

PyPI(Python Package Index)是获取许多Python包的好地方,同时也可以查找pytest插件。当需要pytest插件时,在搜索框中输入pytestpytest--pytest即可,因为大多数 pytest插件的名称要么以pytest-开头,要么以-pytest结尾

Github

https://github.com/pytest-dev

GitHub上的pytest-dev仓库是保存pytest 源代码的地方,在这里也可以找到一些流行的 pytest 插件,这些插件由 pytest 核心团队长期维护。

5.2 安装插件

pytest插件和python包一样,可以用pip工具安装,而且还可以用其他方式来安装。

通过PyPI

PyPI是pip工具的默认下载地址,所以从这里安装是最简单的方式。

默认安装最新版本

比如安装pytest-cov插件,执行命令:

$ pip install pytest-cov
这样会默认自动联网PyPI,并下载和安装最新的稳定版本。

指定版本号

如果你想要某个特定版本的插件,你可以使用==指定该版本。

$ pip install pytest-cov==2.4.0

通过文件

PyPI 的包通常也可以用.tar.gz.whl压缩文件的形式发布,通常被称为tar balls和轮子wheels。如果你无法直接使用pip联网PyPI获取安装包,可以事先把下载好的安装包文件存放到本地,然后通过文件来执行安装。

不需要解压安装包,直接对使用pip命令即可。

安装本地tar文件:

$ pip install pytest-cov-2.4.0.tar.gz

安装本地whl文件:

$ pip install pytest_cov-2.4.0-py2.py3-none-any.whl

通过目录

可以把.tar.gz.whl格式的插件文件统统放在专门的本地目录中,在安装的时候代替PyPI仓库。

比如本地插件文件pytest_cov-2.4.0-py2.py3-none-any.whl是放在some_plugins目录中,对应的安装命令是:

$ pip install --no-index --find-links=./some_plugins/ pytest-cov
--no-index是告诉pip不要连接到PyPI仓库,--find-links=./some_plugins/是告诉pip查找名为some_plugins 的目录。

如果你在本地存放了第三方和自定义插件,并且正在创建用于持续集成或使用tox的虚拟环境,这个技术就特别实用。

更多关于tox和持续集成到知识详见【第七章:配套工具】

本地目录中可以同时存放多个不同版本的安装包,安装时可以通过添加==来指定所需的版本。

$ pip install --no-index --find-links=./some_plugins/ pytest-cov==2.4.0

通过仓库

还可以通过联网git远程仓库安装插件:

$ pip install git+https://github.com/pytest-dev/pytest-cov

可以指定一个版本标签:

$ pip install git+https://github.com/pytest-dev/pytest-cov@v2.4.0

可以指定一个分支:

$ pip install git+https://github.com/pytest-dev/pytest-cov@master
如果我们使用git管理自己的插件代码,或者想要的插件或插件的某个版本不在PyPI 上托管,而是存放在自己的远程仓库,这个方式就很巴适。

5.3 定制插件

许多第三方插件的拥有海量现成的代码,这也是我们使用它们的原因之一,为我们节省了很多时间,不用重复造轮子。然而,对于某些特定的业务场景,肯定还是需要一些特定的夹具,或个性化定制来协助测试工作。

一些需要在多个项目之间共享的夹具,可以通过打包成一个插件来实现轻松地共享。还可以发布自己的插件,将这些创新分享给其他项目,甚至是整个世界。其实挺简单的。

在本节中,我们将对pytest行为进行小小的改动,然后把它打包成插件,最后进行测试,并研究如何发布它。

插件可以包含改变pytest行为的钩子函数。因为 pytest 开发的初衷是允许插件改变pytest的行为,所以有很多钩子函数可用。

更多关于钩子函数详见pytest官方文档: https://doc.pytest.org/en/latest/_modules/_pytest/hookspec.html

我们将在本章中尝试在conftest.py中编写一些插件代码,来练练手。

假设我们有需求有3个:

  • 需要在输出信息顶部添加一些文本,在标题中添加Thanks for running the tests
  • 修改测试结果状态,把所有FAILED状态标志更改为OPPORTUNITY for improvement,将大写F更改为大写O
  • 定义一个命令行选项--nice,控制是否开启以上的新特性。

为了让这些新特性与插件机制的讨论分开,我们将只在conftest.py中进行改动,然后将其转换为可发布的插件。通常情况下,如果只打算在一个项目中使用的这些改动会变得非常实用,可以共享,并形成一个插件。因此,我们首先在conftest.py 插件文件中添加功能,然后运行通过之后,我们再把代码移到一个专门的包中。

让我们回到Tasks 项目。我们在本书第二章2.4捕获异常的部分中编写了一些测试函数,以确保如果有人错误地调用了API 就会引发异常。其实我们漏掉了一些可能的错误条件。

💡 在ch5目录中添加新目录a,然后将ch2/tasks_proj整个目录复制粘贴到其中。接着修改ch5/tasks_proj/tests目录中的本地插件:

# ch5/a/tasks_proj/tests/conftest.py

import pytest
import tasks


@pytest.fixture(scope="session")
def tasks_db_session(tmpdir_factory):
    """Connect to db before tests, disconnect after."""
    temp_dir = tmpdir_factory.mktemp("temp")
    tasks.start_tasks_db(str(temp_dir), "tiny")
    yield
    tasks.stop_tasks_db()


@pytest.fixture()
def tasks_db(tasks_db_session):
    """An empty tasks db."""
    tasks.delete_all()

💡 在ch5/a/tasks_proj/tests/func目录中的测试模块test_api_exceptions.py中添加测试函数:

# ch5/a/tasks_proj/tests/func/test_api_exceptions.py
# 代码片段

from tasks import Task

@pytest.mark.usefixtures('tasks_db')
class TestAdd():
    """Tests related to tasks.add()."""
    def test_missing_summary(self):
        """Should raise an exception if summary missing."""
        with pytest.raises(ValueError):
            tasks.add(Task(owner='bob'))

    def test_done_not_bool(self):
        """Should raise an exception if done is not a bool."""
        with pytest.raises(ValueError):
            tasks.add(Task(summary='summary', done='True'))
让我们运行它,看看结果情况:
$ pytest func/test_api_exceptions.py
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/a/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items                                                          

func/test_api_exceptions.py .....F                                   [100%]

================================= FAILURES =================================
________________________ TestAdd.test_done_not_bool ________________________

self = <ch5.a.tasks_proj.tests.func.test_api_exceptions.TestAdd object at 0x105e81040>

    def test_done_not_bool(self):
        """Should raise an exception if done is not a bool."""
        with pytest.raises(ValueError):
>           tasks.add(Task(summary='summary', done='True'))
E           Failed: DID NOT RAISE <class 'ValueError'>

func/test_api_exceptions.py:52: Failed
========================= short test summary info ==========================
FAILED func/test_api_exceptions.py::TestAdd::test_done_not_bool - Failed:...
======================= 1 failed, 5 passed in 0.02s ========================

现在,让我们使用-k选项专注于测试类TestAdd,再加上-v--tb=no 选项,重新执行一次。

$ ch5/a/tasks_proj/tests
$ pytest func/test_api_exceptions.py -k TestAdd -v --tb=no
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1 -- /Users/xiaofo/Envs/pytest/bin/python3
cachedir: .pytest_cache
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/a/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items / 4 deselected / 2 selected                              

func/test_api_exceptions.py::TestAdd::test_missing_summary PASSED    [ 50%]
func/test_api_exceptions.py::TestAdd::test_done_not_bool FAILED      [100%]

========================= short test summary info ==========================
FAILED func/test_api_exceptions.py::TestAdd::test_done_not_bool - Failed:...
================ 1 failed, 1 passed, 4 deselected in 0.01s =================
可以看到,测试函数test_done_not_bool依旧执行失败了。不过别担心,这是故意造成的,并不打算立即修复。因为,现在我们关注的是让失败提示对开发人员来说更加友好。

定制欢迎语

我们首先将欢迎语添加到头部,可以使用pytest_report_header()钩子函数来实现。

💡 在ch5目录中,把目录a整体拷贝一份粘贴后命名为b,然后ch5/b/tasks_proj/tests目录中的插件文件:

# 代码片段

def pytest_report_header():
"""Thank tester for running tests."""
return "Thanks for running the tests."
当然,只打印一句欢迎语没有什么实际意义。然而,定制结果信息,可以从改变标题可以扩展到添加用户名、指定使用的硬件和测试版本等等。任何你可以转换成字符串的东西,都可以塞到结果信息头部中。

执行命令,看看效果:

$ pytest func/test_api_exceptions.py -k TestAdd -v --tb=no
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1 -- /Users/xiaofo/Envs/pytest/bin/python3
cachedir: .pytest_cache
Thanks for running the tests.
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/b/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items / 4 deselected / 2 selected                              

func/test_api_exceptions.py::TestAdd::test_missing_summary PASSED    [ 50%]
func/test_api_exceptions.py::TestAdd::test_done_not_bool FAILED      [100%]

========================= short test summary info ==========================
FAILED func/test_api_exceptions.py::TestAdd::test_done_not_bool - Failed:...
================ 1 failed, 1 passed, 4 deselected in 0.01s =================
可以看到,结果的第4行已经成功展示了感欢迎语“Thanks for running the tests.”,表明我们的定制结果头部信息的需求已经实现。

定制状态标志

接下来,我们第二个小需求是更改测试的状态信息,将大写F改为大写O,并将FAILED改为OPPORTUNITY。幸运的是,钩子函数pytest_report_teststatus()可以做到。

💡 在ch5/b/tasks_proj/test目录的插件文件conftest.py中,添加新的钩子函数:

# ch5/b/tasks_proj/tests/conftest.py
# 代码片段

# ...

# 钩子函数
def pytest_report_teststatus(report):
    """Turn failures into opportunities."""
    if report.when == 'call' and report.failed:
        return (report.outcome, 'O', 'OPPORTUNITY for improvement')
执行命令,看看常规输出效果:
$ pytest func/test_api_exceptions.py -k TestAdd --tb=no
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1
Thanks for running the tests.
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/b/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items / 4 deselected / 2 selected                              

func/test_api_exceptions.py .O                                       [100%]

========================= short test summary info ==========================
OPPORTUNITY for improvement func/test_api_exceptions.py::TestAdd::test_done_not_bool
================ 1 failed, 1 passed, 4 deselected in 0.02s =================
可以看到,第8行中已经可以看到大写字母O的身影了。并且,第11行显示了状态的备注信息OPPORTUNITY for improvement

执行命令时加上-v选项,再看看详细输出效果:

$ pytest func/test_api_exceptions.py -k TestAdd --tb=no -v
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1 -- /Users/xiaofo/Envs/pytest/bin/python3
cachedir: .pytest_cache
Thanks for running the tests.
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/b/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items / 4 deselected / 2 selected                              

func/test_api_exceptions.py::TestAdd::test_missing_summary PASSED    [ 50%]
func/test_api_exceptions.py::TestAdd::test_done_not_bool OPPORTUNITY for improvement [100%]

========================= short test summary info ==========================
OPPORTUNITY for improvement func/test_api_exceptions.py::TestAdd::test_done_not_bool
================ 1 failed, 1 passed, 4 deselected in 0.01s =================
可以看到,第10行,原先的简写标记O已经展示成完整的名称OPPORTUNITY for improvement了。这表明我们的第二个需求,修改执行结果状态标志,也已经实现。

定制命令选项

我们要做的最后一个小需求是添加一个命令行选项--nice,期望的效果是,只有使用了--nice选项时,失败的测试函数才会展示我们刚刚定制的结果状态标志O和OPPORTUNITY for improvement,否则按照常规标志展示成FFAILED

💡 在ch5目录中,把目录b整体拷贝一份存为c,然后在ch5/b/tasks_proj/tests目录中的插件文件conftest.py中添加新的钩子函数pytest_addoption(parser)

# ch5/c/tasks_proj/tests/conftest.py
# 代码片段

# 钩子函数
def pytest_addoption(parser):
    """Turn nice features on with --nice option."""
    group = parser.getgroup('nice')
    group.addoption("--nice", action="store_true",
    help="nice: turn failures into opportunities")

💡 在ch5/b/tasks_proj/tests目录中的插件文件conftest.py中,修改钩子函数的代码:

# ch5/c/tasks_proj/tests/conftest.py
# 代码片段


# 钩子函数
def pytest_report_header(config):
    """Thank tester for running tests."""
    if config.getoption("nice"):
        return "Thanks for running the tests."


# 钩子函数
def pytest_report_teststatus(report, config):
    """Turn failures into opportunities."""
    if report.when == 'call':
        if report.failed and config.getoption("nice"):
            return (report.outcome, 'O', 'OPPORTUNITY for improvement')

温馨提示:英文原书中获取命令行参数使用的语法pytest.config.getoption("nice"),其中的pytest.config这个全局变量在pytest的5.0版本已经被移除了,对于后续版本的pytest,直接使用config夹具的config.getoption("nice")访问即可。

对于这个定制化的插件,我们目前只使用了3个钩子函数。其实还有更多好玩的,可以参阅pytest官方文档中。

传送门:https://docs.pytest.org/en/latest/writing_plugins.html

手工测试插件
我们可以通过在示例文件中运行插件,手工测试一下新开发出来的插件。

首先,不带--nice选项执行:

$ pytest func/test_api_exceptions.py -k TestAdd --tb=no
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/c/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items / 4 deselected / 2 selected                              

func/test_api_exceptions.py .F                                       [100%]

========================= short test summary info ==========================
FAILED func/test_api_exceptions.py::TestAdd::test_done_not_bool - Failed:...
================ 1 failed, 1 passed, 4 deselected in 0.02s =================

然后,带上--nice选项执行:

$ pytest func/test_api_exceptions.py -k TestAdd --tb=no --nice
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1
Thanks for running the tests.
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/c/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items / 4 deselected / 2 selected                              

func/test_api_exceptions.py .O                                       [100%]

========================= short test summary info ==========================
OPPORTUNITY for improvement func/test_api_exceptions.py::TestAdd::test_done_not_bool
================ 1 failed, 1 passed, 4 deselected in 0.02s =================

然后,再带上-v选项再次执行:

$ pytest func/test_api_exceptions.py -k TestAdd --tb=no --nice -v
输出结果:
=========================== test session starts ============================
platform darwin -- Python 3.9.7, pytest-6.2.0, py-1.10.0, pluggy-0.13.1 -- /Users/xiaofo/Envs/pytest/bin/python3
cachedir: .pytest_cache
Thanks for running the tests.
rootdir: /Users/xiaofo/coding/Personal/studypytest/ch5/c/tasks_proj/tests, configfile: pytest.ini
plugins: allure-pytest-2.9.45
collected 6 items / 4 deselected / 2 selected                              

func/test_api_exceptions.py::TestAdd::test_missing_summary PASSED    [ 50%]
func/test_api_exceptions.py::TestAdd::test_done_not_bool OPPORTUNITY for improvement [100%]

========================= short test summary info ==========================
OPPORTUNITY for improvement func/test_api_exceptions.py::TestAdd::test_done_not_bool
================ 1 failed, 1 passed, 4 deselected in 0.01s =================
妙啊!我们的三个需求,简单地在conftest.py 文件中编写十几行代码就搞定了。

接下来,我们将把这段代码移动到一个标准的插件项目中。

5.4 打包插件

Creating an Installable Plugin

与其他人分享插件是值得推荐的,通过这个过程,我们也可以学会阅读开源插件的代码,并且更有能力判断它们是否会有助于你。

在这本书中不会完全介绍Python打包和发布,因为这个主题在其他地方已经有了很好的文档。从本地配置插件变成一个可以使用pip工具安装的插件,还是一个很小的任务。

参考文档:http://python-packaging.readthedocs.io 参考文档:https://www.pypa.io

首先,我们需要创建一个新目录来放置插件代码。因为这是一个给--nice选项制作的插件,让我们命名为pytest-nice

💡 在ch5目录中依次创建必要的目录和文件:

pytest-nice
├── LICENCE
├── README.rst
├── pytest_nice.py
├── setup.py
└── tests
    ├── conftest.py
    └── test_nice.py

pytest_nice.py

💡 我们将ch5/c/tasks_proj/tests/conftest.py中的三个钩子函数代码复制到pytest_nice.py文件中。

# ch5/pytest-nice/pytest_nice.py

"""Code for pytest-nice plugin."""


def pytest_addoption(parser):
    """Turn nice features on with --nice option."""
    group = parser.getgroup('nice')
    group.addoption("--nice", action="store_true",
                    help="nice: turn failures into opportunities")


# 钩子函数
def pytest_report_header(config):
    """Thank tester for running tests."""
    if config.getoption("nice"):
        return "Thanks for running the tests."


# 钩子函数
def pytest_report_teststatus(report, config):
    """Turn failures into opportunities."""
    if report.when == 'call':
        if report.failed and config.getoption("nice"):
            return (report.outcome, 'O', 'OPPORTUNITY for improvement')

setup.py

setup.py文件中,我们需要根据项目情况,编写一个setup()函数:

# ch5/pytest-nice/setup.py

"""Setup for pytest-nice plugin."""

from setuptools import setup

setup(
    name='pytest-nice',
    version='0.1.0',
    description='A pytest plugin to turn FAILURE into OPPORTUNITY',
    url='https://wherever/you/have/info/on/this/package',
    author='Your Name',
    author_email='your_email@somewhere.com',
    license='proprietary',
    py_modules=['pytest_nice'],
    install_requires=['pytest'],
    entry_points={'pytest11': ['nice = pytest_nice', ], },
)
如果你打算在线发布,那么需要在setup.py中配置更多信息。对于一个小团队,或者仅仅是自用来说,这些配置就足够了。

setup()的设置还支持更多的参数,我们这只展示必填的字段。

字段 是否必填 说明
name 必填 表示插件项目名称,即setup.py文件所在目录名称。
version 必填 表示插件版本,完全由作者决定
description 必填 表示插件的描述信息
url 必填 表示插件文档地址
author 必填 等同于maintainer,表示插件作者名称
author_email 必填 等同于maintainer_email,表示插件作者邮箱
license 必填 表示许可证名称。可以是开源许可证名称,作者名字,或公司名字,内容不限
py_modules 必填 表示插件代码文件名称列表。如果存在多个模块,建议通过包统一管理。

虽然py_modules支持的是一个列表,可以填写多个模块名称,但是如果有多个模块的时候,建议使用包来代替,并把所有的模块放在一个包中。

以上列举参数都是标准的,和所有 Python安装程序所用的参数相同。pytest 插件与python包的不同之处在于 entry_points 参数。

我们看看这里的entry_points={'pytest11': ['nice = pytest_nice', ], }

  • entry_pointssetuptools的标准特性
  • pytest11pytest要寻找的特殊标识符,
  • ['nice = pytest_nice', ]:其中nice是插件的名称,pytest_nice是插件代码所在的模块的名称。如果我们使用的是一个包,这里的正确写法是:['PLUGIN_NAME = xxx.pluginmodule',]

README

接下来看看README.rst文件。README文件是setuptools必不可少的。如果你忽略它,将会出现下面的警告信息:

...
warning: sdist: standard file not found: should have one of README,
README.rst, README.txt
...
无论如何,在插件中保留一份关于项目的介绍信息README文件是一个好习惯。文件的格式可以是.rst,也可以是.md

参考内容如下:

pytest-nice : A pytest plugin
=============================
Makes pytest output just a bit nicer during failures.

Features
--------

- Includes user name of person running tests in pytest output.
- Adds ``--nice`` option that:
- turns ``F`` to ``O``
- with ``-v``, turns ``FAILURE`` to ``OPPORTUNITY for improvement``

Installation
------------
Given that our pytest plugins are being saved in .tar.gz form in the
shared directory PATH, then install like this:

::
    $ pip install PATH/pytest-nice-0.1.0.tar.gz
    $ pip install --no-index --find-links PATH pytest-nice


Usage
-----

::
    $ pytest --nice
关于README文件中应该包含什么内容,观点五花八门。以上是一个相当简单的版本,仅供参考。

LICENSE

许可证文件。

5.5 插件自测

Testing Plugins

插件的代码也需要像其他功能代码一样进行测试。然而,测试一个测试工具的变化确实有点棘手。在本章前面定制开发pytest-new插件代码时,我们通过编写示例测试文件手动测试它,运行pytest,并查看输出以确保它是正确的。

我们还可以使用 pytester 插件自动完成同样的事情,这个插件已经附带了pytest,但是默认情况下是禁用的。

当前项目结构:

pytest-nice
├── LICENCE
├── README.rst
├── pytest_nice.py
├── setup.py
└── tests
    ├── conftest.py
    └── test_nice.py

现在来看看pytest-nice插件项目的tests目录。测试目录中有两个文件: conftest.pytest_nice.py

如果要使用pytester,则需要在conftest.py中添加一行代码。

"""pytester is needed for testing plugins."""
pytest_plugins = 'pytester'
这样就会启用pytester插件,然后我们可以在测试函数中使用testdir夹具了。

通常,插件自动化测试的过程类似之前手工测试的步骤:

  1. 创建一个测试文件。
  2. 在测试文件所在目录对比运行pytest,分别带上或不带一些选项。
  3. 检查执行结果:结果代码是0表示全部通过,是1表示有部分失败。

💡 在ch5/pytest-nice/tests目录的测试模块test_pytester.py中,添加新的测试函数test_pass_fail()

# ch5/pytest-nice/tests/test_pytester.py

def test_pass_fail(testdir):

    # 创建一个临时的pytest测试模块
    testdir.makepyfile("""
        def test_pass():
            assert 1 == 1
        def test_fail():
            assert 1 == 2
    """)

    # 运行 pytest
    result = testdir.runpytest()

    # fnmatch_lines 会在内部进行断言
    result.stdout.fnmatch_lines([
        '*.F*',  # . 表示执行通过, F 表示执行失败
    ])

    # 确保用例集的返回结束码是1
    assert result.ret == 1
代码解释:testdir夹具会自动创建一个放置测试文件的临时目录。makepyfile()方法允许我们放入测试文件的内容。在这个例子中,我们创建了两个测试用例: 一个预期通过,一个预期失败。然后,我们使用pytest运行含有 testdir.runpytest()的测试文件。如果有需要的话,可以加上选项执行。返回值result可以用于后续的断言,它的类型是RunResult

通常情况下需要关注的是结果中的 stdoutret 属性。使用 fnmatch_lines,能够模拟手工检查输出结果一样,传入一个期望在输出中看到的字符串列表。然后,测试通过的会话的ret值为0,测试失败会话的 ret 值为1。另外,传递到fnmatch_lines()函数的字符串可以包含通配符*

温馨提示:英文原版此处的匹配表达式都存在小小的勘误,直接运行将会导致执行断言失败。解决办法就是,将result.stdout.fnmatch_lines中的匹配符的结尾处都补上通配符*

  • 原文的匹配表达式:'*.F'
  • 正确的匹配表达式:'*.F*'

执行命令:

$ pytest test_pytester.py -v
输出结果:
========================== test session starts ==========================
platform win32 -- Python 3.8.8, pytest-7.1.1, pluggy-1.0.0 -- d:\system\en
vs\mytasks\scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\Coding\Gitees\studypytest\ch5\pytest-nice
plugins: nice-0.1.0
collected 1 item                                                         

test_pytester.py::test_pass_fail PASSED                            [100%]

=========================== 1 passed in 0.11s ===========================
可以看到,测试通过了。

我们可以按照这种写法,来为pytest-nice插件编写更多的测试函数。不要简单复制这些代码,考虑制作一个可复用的夹具。

💡 在ch5/pytest-nice/tests的测试模块test_nice.py中,添加新的夹具sample_test()

# 代码片段

@pytest.fixture()
def sample_test(testdir):
    testdir.makepyfile("""
        def test_pass():
            assert 1 == 1
        def test_fail():
            assert 1 == 2
    """)
    return testdir
现在,就可以开始动手编写插件的其他测试函数了,我们可以使用sample_test夹具作为已经包含示例测试文件的目录。

验证命令行选项

💡 在ch5/pytest-nice/tests目录的测试模块test_nice.py中,添加新的测试函数:

# 代码片段


def test_with_nice(sample_test):
    result = sample_test.runpytest('--nice')
    result.stdout.fnmatch_lines(['*.O*', ])  # . for Pass, O for Fail
    assert result.ret == 1


def test_not_with_nice(sample_test):
    result = sample_test.runpytest()
    result.stdout.fnmatch_lines(['*.F*', ])  # . for Pass, F for Fail
    assert result.ret == 1
代码解释:测试函数test_with_nice()是带--nice选项执行,test_not_with_nice()是不带--nice选项执行。

验证状态标志

💡 在ch5/pytest-nice/tests目录的测试模块test_nice.py中,添加新的测试函数:

# 代码片段

def test_with_nice_verbose(sample_test):
    result = sample_test.runpytest('-v', '--nice')
    result.stdout.fnmatch_lines([
        '*::test_fail OPPORTUNITY for improvement*',
    ])
    assert result.ret == 1


def test_not_nice_verbose(sample_test):
    result = sample_test.runpytest('-v')
    result.stdout.fnmatch_lines(['*::test_fail FAILED*'])
    assert result.ret == 1
代码说明:以上两个测试函数是用来验证带有-v参数执行后,详细结果中的标志名称是否符合预期。

验证欢迎语

让我们确保在标题中展示欢迎语的逻辑。

💡 在ch5/pytest-nice/tests目录的测试模块test_nice.py中,添加新的测试函数:

# 代码片段

def test_header(sample_test):
    result = sample_test.runpytest('--nice')
    result.stdout.fnmatch_lines(['Thanks for running the tests.'])


def test_header_not_nice(sample_test):
    result = sample_test.runpytest()
    thanks_message = 'Thanks for running the tests.'
    assert thanks_message not in result.stdout.str()
把它放在一个单独的测试函数中,这样一个测试函数只检查一个测试点。

验证帮助文档

最后,让我们检查插件的帮助文档。

# 代码片段

def test_help_message(testdir):
    result = testdir.runpytest('--help')
    # fnmatch_lines does an assertion internally
    result.stdout.fnmatch_lines([
        'nice:',
        '*--nice*nice: turn failures into opportunities',
    ])
温馨提示:英文原版此处的匹配表达式都存在小小的勘误

  • 原文的匹配表达式:'*--nice*nice: turn FAILED into OPPORTUNITY for improvement
  • 正确的匹配表达式:'*--nice*nice: turn failures into opportunities'

为了运行测试,首先要安装被测插件pytest-nice

执行命令:

$ cd ch5/pytest-nice/
$ pip install .
输出结果:
...
Installing collected packages: pytest-nice
  Running setup.py install for pytest-nice ... done
Successfully installed pytest-nice-0.1.0

安装成功之后,开始测试:

$ cd tests
$ pytest test_nice.py -v
输出结果:
========================== test session starts ==========================
platform win32 -- Python 3.8.8, pytest-7.1.1, pluggy-1.0.0 -- d:\system\en
vs\mytasks\scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\Coding\Gitees\studypytest\ch5\pytest-nice
plugins: nice-0.1.0
collected 7 items                                                        

test_nice.py::test_with_nice PASSED                                [ 14%]
test_nice.py::test_not_with_nice PASSED                            [ 28%]
test_nice.py::test_with_nice_verbose PASSED                        [ 42%]
test_nice.py::test_not_nice_verbose PASSED                         [ 57%]
test_nice.py::test_header PASSED                                   [ 71%]
test_nice.py::test_header_not_nice PASSED                          [ 85%]
test_nice.py::test_help_message PASSED                             [100%]

=========================== 7 passed in 0.42s ===========================
妙啊! 所有的关于插件选项的测试函数都执行通过了。

最后,我们可以像其他 Python包一样,执行pip命令即可卸载插件。

$ pip uninstall pytest-nice

另外,了解更多关于插件测试的一个途径是,通过查阅PyPI上的可用的pytest插件的测试源码。

5.6 发布插件

我们的插件就快完成了。

打包发布

在命令行中,我们可以利用setup.py文件对插件进行打包。

$ cd ch5/pytest-nice
$ python setup.py sdist
sdistsource distribution的缩写,表示源码发布。

然后,在pytest-nice下的dist 目录中,会出现一个名为 pytest-nice-0.1.0.tar.gz 的压缩包文件。这个文件就可以用来通过本地安装的方式安装我们的pytest-nice插件。

通过本次文件安装插件:

$ pip install dist/pytest-nice-0.1.0.tar.gz
并且,你可以将.tar.gz 文件放在任何地方,以便使用和共享。

PyPI仓库

Distributing Plugins Through PyPI

如果你想与全世界分享你的插件,实际上,还有很多步骤。但是,由于本书并不专注于为开源做贡献,我建议你查阅Python的打包操作指引中的详细说明。

传送门:https://packaging.python.org/distributing

cookiecutter

另一个值得推荐的是使用cookiecutter-pytest-plugin:

$ pip install cookiecutter
$ cookiecutter https://github.com/pytest-dev/cookiecutter-pytest-plugin
这个项目首先问你一些关于你的插件的问题。然后创建一个很好的目录供你浏览和填充你的代码。虽然浏览这些内容不在本书的关注范围,但是还是希望大家记住这个项目。它是由核心pytest 人员支持的,他们将确保这个项目保持最新。

传送门:https://github.com/pytest-dev/cookiecutter-pytest-plugin


到目前为止,你在这本书中使用了很多conftest.py。还有一些配置文件会影响 pytest 的运行方式,比如 pytest.ini。在下一章中,你将运行不同的配置文件,并了解如何使你的测试工作更容易。