基于Pytest的代码结构,可以借助hook函数来实现定制和扩展插件,将Fixture和Hook函数添加到conftest.py文件里,就已经创建了一个本地conftest插件,然后将conftest.py文件转换为可安装的插件然后再与其他人分享。

获取第三方插件

已经有很多人开发了自己的插件,通过如下地址可以找到很多实用或者有意思的插件供我们使用。
https://docs.pytest/en/latest/plugins.html
https://pypi.python
https://github/pytest-dev

安装插件

常规安装

pip install pytest-cov
pip install pytest-cov==2.5.1
pip install pytest-cov-2.5.1.tar.gz
pip install pytest_cov-2.5.1-py2.py3-none-any.whl

本地安装插件

mkdir some_plugins
cp pytest_cov-2.5.1-py2.py3-none-any.whl some_plugins/
pip install --no-index --find-links=./some_plugins/ pytest-cov
pip install --no-index --find-links=./some_plugins/ pytest-cov==2.5.1

–no-index 告诉pip不要链接PyPI
–find-links=./some_plugins/ 告诉pip安装时候查找安装文件的目录

从Git安装插件

pip install git+https://github/pytest-dev/pytest-cov
pip install git+https://github/pytest-dev/pytest-cov@v2.5.1
pip install git+https://github/pytest-dev/pytest-cov@master

编写自己的插件

开发pytest的目的之一就是用插件改变pytest的运行方式,而hook函数是编写插件的利器之一,可以通过hookspect在本例中,会将错误信息修改为OPPORTINITY for improvement,而将F的状态改为O,并在标题中添加Thanks for running the tests,然后使用–nice来打开新增功能。
还是以Task项目为例,列举两个例测试,代码如下:

"""Test for expected exceptions from using the API wrong."""
import pytest
import tasks
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'))

然后我们在conftest.py代码新增:

@pytest.fixture(scope='session')
def tasks_db_session(tmpdir_factory, request):
    """Connect to db before tests, disconnect after."""
    temp_dir = tmpdir_factory.mktemp('temp')
    tasks.start_tasks_db(str(temp_dir), 'tiny')
    yield  # this is where the testing happens
    tasks.stop_tasks_db()


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

执行结果:

D:\PythonPrograms\Python_Pytest\TestScripts>pytest test_api_exceptions.py
============================= test session starts =============================
platform win32 -- Python 3.7.2, pytest-4.0.2, py-1.8.0, pluggy-0.12.0
rootdir: D:\PythonPrograms\Python_Pytest\TestScripts, inifile:
plugins: allure-adaptor-1.7.10, cov-2.7.1
collected 2 items

test_api_exceptions.py .F                                                [100%]

================================== FAILURES ===================================
_________________________ TestAdd.test_done_not_bool __________________________

self = <TestScripts.test_api_exceptions.TestAdd object at 0x000000B58386DF28>

    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'>

test_api_exceptions.py:19: Failed
================= 1 failed, 1 passed in 0.13 seconds ======================

接下来借助hook函数,将thanks …消息添加进去并且将F更改为O,将FAILED改为OPPORTUNITY for improvement,在conftest.py新增:

def pytest_report_header():
    """Thank tester for running tests."""
    return "Thanks for running the tests.

def pytest_report_teststatus(report):
    """Turn failures into opportunities."""
    if report.when == 'call' and report.failed:
        return (report.outcome, 'O', 'OPPORTUNITY for improvement')

在此执行:

D:\PythonPrograms\Python_Pytest\TestScripts>pytest -v --tb=no test_api_exceptions.py
===================== test session starts =============================
platform win32 -- Python 3.7.2, pytest-4.0.2, py-1.8.0, pluggy-0.12.0 -- c:\python37\python.exe
cachedir: .pytest_cache
Thanks for running the tests.
rootdir: D:\PythonPrograms\Python_Pytest\TestScripts, inifile:
plugins: allure-adaptor-1.7.10, cov-2.7.1
collected 2 items

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

================ 1 failed, 1 passed in 0.13 seconds ======================

使用pytest_addoption函数添加命令行选项,修改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")


def pytest_report_header():
    """Thank tester for running tests."""
    if pytest.config.getoption('nice'):
        return "Thanks for running the tests."


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

同时在conftest.py同路径下新增pytest.ini文件,并在文件中写入:

[pytest]
nice=True

然后使用刚刚创建的–nice再次执行代码:

D:\PythonPrograms\Python_Pytest\TestScripts>pytest --nice --tb=no test_api_exceptions.py
===================== test session starts =============================
platform win32 -- Python 3.7.2, pytest-4.0.2, py-1.8.0, pluggy-0.12.0
Thanks for running the tests.
rootdir: D:\PythonPrograms\Python_Pytest\TestScripts, inifile: pytest.ini
plugins: allure-adaptor-1.7.10, cov-2.7.1
collected 2 items

test_api_exceptions.py .O                                                [100%]

=============== 1 failed, 1 passed in 0.14 seconds ======================

加上-v再次执行一次:

(venv) E:\Programs\Python\Python_Pytest\TestScripts>pytest -v --nice --tb=no test_api_exceptions.py
============= test session starts ===============================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0 -- c:\python37\python.exe
cachedir: .pytest_cache
Thanks for running the tests.
rootdir: E:\Programs\Python\Python_Pytest\TestScripts, inifile: pytest.ini
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, instafail-0.4.1, forked-1.0.2, emoji-0.2.0, allure-pytest-2.6.3
collected 2 items                                                                                                                                                                                                                      

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

在本例中我们使用了三个hook函数,实际上还有很多可用的hook,地址为writing_plugins

创建可安装的插件

将刚刚编写的插件变成可安装的插件并不难,首先在项目中创建这样一个目录结构

pytest-nice
|-------LICENCE
|-------README.md
|-------pytest_nice.py
|-------setup.py
|-------test
		 |-------conftest.py
		 |-------test_nice.py

然后将conftest.py中的hook函数转移到pytest_nice.py文件中,并将其从conftest.py中删除,然后pytest_nice.py文件中代码如下:

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

import pytest


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


def pytest_report_header():
    """Thank tester for running tests."""
    if pytest.config.getoption('nice'):
        return "Thanks for running the tests."


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

在setup.py文件里调用setup()函数

"""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='davieyang',
    author_email='davieyang@qq',
    license='proprietary',
    py_modules=['pytest_nice'],
    install_requires=['pytest'],
    entry_points={'pytest11': ['nice = pytest_nice', ], },
)

实际上setup()函数还有很多用于提供其他信息的参数,有兴趣可以自行研究这个函数。
setup()函数里的所有参数都是标准的,可用于所有python的安装程序,pytest插件的不同之处在于entry_point参数。

entry_points={'pytest11': ['nice = pytest_nice', ], },

entry_points是setuptools的标准功能,pytest11是一个特殊的标识符,通过这个设置,可以告诉pytest插件名称为nice,模块名称是pytest_nice,如果使用了包,则设置可以改成:

entry_points={'pytest11':['name_of_plugin=myprojectpluginmodule',],},

README.rst,setuptools要求必须提供README.rst文件,如果没有提供则会有警告
README.rst文件内容如下:

pytest-nice:A pytest plugin
===========================================================
Makes pytest output just a bit nicer during failures.
Features
--------
-Includs user name of person running tests in pytest output.
-Add "--nice" option that:
	-turns "F" to "O"
	-with "-v", turns "FAILURE" to "OPPORTUNITY for improvement"
Installation
------------
Given that our pytst plugins are being saved in .rat.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

插件编写好后,便可以执行命令安装:

(venv) E:\Programs\Python\Python_Pytest>pip install pytest-nice/
Processing e:\programs\python\python_pytest\pytest-nice
Requirement already satisfied: pytest in c:\python37\lib\site-packages (from pytest-nice==0.1.0) (4.5.0)
Requirement already satisfied: colorama; sys_platform == "win32" in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (0.4.1)
Requirement already satisfied: py>=1.5.0 in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (1.8.0)
Requirement already satisfied: six>=1.10.0 in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (1.12.0)
Requirement already satisfied: attrs>=17.4.0 in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (19.1.0)
Requirement already satisfied: wcwidth in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (0.1.7)
Requirement already satisfied: setuptools in c:\users\davieyang\appdata\roaming\python\python37\site-packages (from pytest->pytest-nice==0.1.0) (41.0.1)
Requirement already satisfied: more-itertools>=4.0.0; python_version > "2.7" in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (7.0.0)
Requirement already satisfied: pluggy!=0.10,<1.0,>=0.9 in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (0.11.0)
Requirement already satisfied: atomicwrites>=1.0 in c:\python37\lib\site-packages (from pytest->pytest-nice==0.1.0) (1.3.0)
Building wheels for collected packages: pytest-nice
  Building wheel for pytest-nice (setup.py) ... done
  Stored in directory: C:\Users\davieyang\AppData\Local\pip\Cache\wheels\db\f5\74\45aa97afa6c4141a69713b4b412273cbceaa0bb48770dd00d9
Successfully built pytest-nice
Installing collected packages: pytest-nice
Successfully installed pytest-nice-0.1.0

测试插件

既可以使用前边的用的方式,直接编写测试函数执行,在结果中查看插件是否运行正确,也可以使用一个名为pytester的插件来自动完成同样的工作,这个插件是pytest自带的,但默认是关闭的。
在test目录里的conftest.py文件中添加如下代码,即可开启pytester

"""pytester is needed for testing plugins."""
pytest_plugins = 'pytester'

如此便开启了pytester,我们要使用的fixture叫做testdir,开启pytester之后就可以使用了。

"""Testing the pytest-nice plugin."""

import pytest


def test_pass_fail(testdir):

    # create a temporary pytest test module
    testdir.makepyfile("""
        def test_pass():
            assert 1 == 1

        def test_fail():
            assert 1 == 2
    """)

    # run pytest
    result = testdir.runpytest()

    # fnmatch_lines does an assertion internally
    result.stdout.fnmatch_lines([
        '*.F*',  # . for Pass, F for Fail
    ])

    # make sure that that we get a '1' exit code for the testsuite
    assert result.ret == 1

testdir自动创建了一个临时的目录用来互访测试文件,他有一个名为makepyfile()的方法,允许我们写入测试文件
使用testdir.runpytest()方法来运行pytest并测试新的测试文件,还可以设置结果选项,返回值类型是RunResult
在该文件中新增测试方法:

@pytest.fixture()
def sample_test(testdir):
    testdir.makepyfile("""
        def test_pass():
            assert 1 == 1

        def test_fail():
            assert 1 == 2
    """)
    return testdir


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_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


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 FAILED into OPPORTUNITY for improvement',
    ])

命令行执行该用例:

(venv) E:\Programs\Python\Python_Pytest\pytest-nice\tests>pytest -v
===================================== test session starts ===================================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0 -- c:\python37\python.exe
cachedir: .pytest_cache
rootdir: E:\Programs\Python\Python_Pytest\pytest-nice
plugins: xdist-1.29.0, timeout-1.3.3, repeat-0.8.0, nice-0.1.0, instafail-0.4.1, forked-1.0.2, emoji-0.2.0, allure-pytest-2.6.3
collected 7 items                                                                                                                                                                                                                      

test_nice.py::test_pass_fail PASSED                                                                                                                                                                                              [ 14%]
test_nice.py::test_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%]

创建发布包

执行命令:python setup.py sdist,结果如下:

(venv) E:\Programs\Python\Python_Pytest\pytest-nice>python setup.py sdist
running sdist
running egg_info
creating pytest_nice.egg-info
writing pytest_nice.egg-info\PKG-INFO
writing dependency_links to pytest_nice.egg-info\dependency_links.txt
writing entry points to pytest_nice.egg-info\entry_points.txt
writing requirements to pytest_nice.egg-info\requires.txt
writing top-level names to pytest_nice.egg-info\top_level.txt
writing manifest file 'pytest_nice.egg-info\SOURCES.txt'
reading manifest file 'pytest_nice.egg-info\SOURCES.txt'
writing manifest file 'pytest_nice.egg-info\SOURCES.txt'
running check
creating pytest-nice-0.1.0
creating pytest-nice-0.1.0\pytest_nice.egg-info
copying files to pytest-nice-0.1.0...
copying README.rst -> pytest-nice-0.1.0
copying pytest_nice.py -> pytest-nice-0.1.0
copying setup.py -> pytest-nice-0.1.0
copying pytest_nice.egg-info\PKG-INFO -> pytest-nice-0.1.0\pytest_nice.egg-info
copying pytest_nice.egg-info\SOURCES.txt -> pytest-nice-0.1.0\pytest_nice.egg-info
copying pytest_nice.egg-info\dependency_links.txt -> pytest-nice-0.1.0\pytest_nice.egg-info
copying pytest_nice.egg-info\entry_points.txt -> pytest-nice-0.1.0\pytest_nice.egg-info
copying pytest_nice.egg-info\requires.txt -> pytest-nice-0.1.0\pytest_nice.egg-info
copying pytest_nice.egg-info\top_level.txt -> pytest-nice-0.1.0\pytest_nice.egg-info
Writing pytest-nice-0.1.0\setup.cfg
creating dist
Creating tar archive
removing 'pytest-nice-0.1.0' (and everything under it)

工程结构中便会自动生成如下两个文件夹

dist目录下的xxx.tar.gz是用来安装的安装文件,可以在任何可安装的地方安装包括当前目录,更方便用于共享。

通过共享目录分发插件

pip支持从共享目录安装,选择一个目录,将xxx.tar.gz文件放进去,例如放在myplugins目录里
然后执行命令:

pip install --no-index --find-links myplugins pytest-nice

--no-index告诉pip不要查找PyPI
--find-links myplugins告诉pip在myplugins中查找要安装的软件包
如果你更新了插件,则可以使用–upgrade选项更新安装

pip install --upgrade --no-index --find-links myplugins pytest-nice

通过PyPI发布插件

可以通过官方地址distributing查询更详细的通过PyPI发布插件的方法
除了python官方地址,还有个非常好的分享插件的地方是cookiecutter,cookiecutter

pip install cookiecutter
cookiecutter https://github.com/pytest-dev/cookiecutter-pytest-plugin

更多推荐

Pytest单元测试系列[v1.0.0][编写插件及分享]