Plugins
第5章:插件¶
尽管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官网¶
这是pytest官方文档地址,上面有一个讨论安装和使用pytest 插件的页面,列出了一些常见的插件。
PyPI¶
PyPI
(Python Package Index)是获取许多Python包的好地方,同时也可以查找pytest插件。当需要pytest插件时,在搜索框中输入pytest
、pytest-
或-pytest
即可,因为大多数 pytest插件的名称要么以pytest-
开头,要么以-pytest
结尾
Github¶
GitHub上的pytest-dev
仓库是保存pytest 源代码的地方,在这里也可以找到一些流行的 pytest 插件,这些插件由 pytest 核心团队长期维护。
5.2 安装插件¶
pytest插件和python包一样,可以用pip工具安装,而且还可以用其他方式来安装。
通过PyPI¶
PyPI
是pip工具的默认下载地址,所以从这里安装是最简单的方式。
默认安装最新版本
比如安装pytest-cov
插件,执行命令:
$ pip install pytest-cov
指定版本号
如果你想要某个特定版本的插件,你可以使用==
指定该版本。
$ 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
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 =================
定制状态标志¶
接下来,我们第二个小需求是更改测试的状态信息,将大写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 =================
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 =================
OPPORTUNITY for improvement
了。这表明我们的第二个需求,修改执行结果状态标志,也已经实现。
定制命令选项¶
我们要做的最后一个小需求是添加一个命令行选项--nice
,期望的效果是,只有使用了--nice
选项时,失败的测试函数才会展示我们刚刚定制的结果状态标志O和OPPORTUNITY for improvement
,否则按照常规标志展示成F
和FAILED
。
💡 在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官方文档中。
手工测试插件
我们可以通过在示例文件中运行插件,手工测试一下新开发出来的插件。
首先,不带--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 =================
接下来,我们将把这段代码移动到一个标准的插件项目中。
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_points
是setuptools
的标准特性pytest11
是pytest
要寻找的特殊标识符,['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
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.py
和 test_nice.py
。
如果要使用pytester
,则需要在conftest.py
中添加一行代码。
"""pytester is needed for testing plugins."""
pytest_plugins = 'pytester'
pytester
插件,然后我们可以在测试函数中使用testdir
夹具了。
通常,插件自动化测试的过程类似之前手工测试的步骤:
- 创建一个测试文件。
- 在测试文件所在目录对比运行pytest,分别带上或不带一些选项。
- 检查执行结果:结果代码是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
。
通常情况下需要关注的是结果中的 stdout
和 ret
属性。使用 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
sdist
是source distribution
的缩写,表示源码发布。
然后,在pytest-nice
下的dist
目录中,会出现一个名为 pytest-nice-0.1.0.tar.gz
的压缩包文件。这个文件就可以用来通过本地安装的方式安装我们的pytest-nice
插件。
通过本次文件安装插件:
$ pip install dist/pytest-nice-0.1.0.tar.gz
PyPI仓库¶
Distributing Plugins Through PyPI
如果你想与全世界分享你的插件,实际上,还有很多步骤。但是,由于本书并不专注于为开源做贡献,我建议你查阅Python的打包操作指引中的详细说明。
cookiecutter¶
另一个值得推荐的是使用cookiecutter-pytest-plugin
:
$ pip install cookiecutter
$ cookiecutter https://github.com/pytest-dev/cookiecutter-pytest-plugin
传送门:https://github.com/pytest-dev/cookiecutter-pytest-plugin
到目前为止,你在这本书中使用了很多conftest.py
。还有一些配置文件会影响 pytest 的运行方式,比如 pytest.ini
。在下一章中,你将运行不同的配置文件,并了解如何使你的测试工作更容易。