基于Pytest的代码结构,可以借助hook函数来实现定制和扩展插件,将Fixture和Hook函数添加到conftest.py文件里,就已经创建了一个本地conftest插件,然后将conftest.py文件转换为可安装的插件然后再与其他人分享。
已经有很多人开发了自己的插件,通过如下地址可以找到很多实用或者有意思的插件供我们使用。
https://docs.pytest.org/en/latest/plugins.html
https://pypi.python.org
https://github.com/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安装时候查找安装文件的目录
pip install git+https://github.com/pytest-dev/pytest-cov
pip install git+https://github.com/pytest-dev/[email protected]
pip install git+https://github.com/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='[email protected]',
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
可以通过官方地址distributing查询更详细的通过PyPI发布插件的方法
除了python官方地址,还有个非常好的分享插件的地方是cookiecutter,cookiecutter
pip install cookiecutter
cookiecutter https://github.com/pytest-dev/cookiecutter-pytest-plugin