Pytest单元测试系列[v1.0.0][Pytest高级应用]

编写测试函数

一个规范的测试方法一定带有断言,在使用pytest时,可以直接使用Python自带的assert关键字

Pytest Unittest
assert something assertTrue(something)
assert a==b assertEqual(a,b)
assert a<=b assertLessEqual(a,b)
... ...
Pytest允许在assert关键字添加任意表达式,表达式的值通过bool转换后等于False则表示断言失败,测试用例执行失败;如果等于True则表示断言成功,测试用例执行成功。

重写assert

pytest可以重写assert关键字,它可以截断对原生assert的调用,替换为pytest定义的assert,从而展示更详细的信息和细节。

python 复制代码
from collections import namedtuple
Task = namedtuple('Task', ['summary','owner','done','id'])
# __new__.__defaults__创建默认的Task对象
Task.__new__.__defaults__ = (None, None, False, None)

import pytest


def test_task_equality():
    t1 = Task('sit there', 'brain')
    t2 = Task('do something', 'okken')
    print(t1)
    print(t2)
    assert t1 == t2


def test_dict_equal():
    t1_dict = Task('make sandwich', 'okken')._asdict()
    t2_dict = Task('make sandwich', 'okkem')._asdict()
    print(t1_dict)
    print(t2_dict)
    assert t1_dict == t2_dict

执行结果如下:

python 复制代码
E:\Programs\Python\Python_Pytest\TestScripts>pytest test_three.py
=========================================== test session starts ==============================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
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_three.py FF                                                                                                                                                     [100%]

================================================ FAILURES ====================================================
___________________________________________ test_task_equality _______________________________________________

    def test_task_equality():
        t1 = Task('sit there', 'brain')
        t2 = Task('do something', 'okken')
        print(t1)
        print(t2)
>       assert t1 == t2
E       AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None)
E         At index 0 diff: 'sit there' != 'do something'
E         Use -v to get the full diff

test_three.py:14: AssertionError
---------------------------------------- Captured stdout call ------------------------------------------------
Task(summary='sit there', owner='brain', done=False, id=None)
Task(summary='do something', owner='okken', done=False, id=None)
___________________________________________ test_dict_equal __________________________________________________

    def test_dict_equal():
        t1_dict = Task('make sandwich', 'okken')._asdict()
        t2_dict = Task('make sandwich', 'okkem')._asdict()
        print(t1_dict)
        print(t2_dict)
>       assert t1_dict == t2_dict
E       AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)])
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'owner': 'okken'} != {'owner': 'okkem'}
E         Use -v to get the full diff

test_three.py:22: AssertionError
------------------------------------------- Captured stdout call ---------------------------------------------
OrderedDict([('summary', 'make sandwich'), ('owner', 'okken'), ('done', False), ('id', None)])
OrderedDict([('summary', 'make sandwich'), ('owner', 'okkem'), ('done', False), ('id', None)])
========================================== 2 failed in 0.20 seconds===========================================

加上参数-v,执行结果如下:

python 复制代码
E:\Programs\Python\Python_Pytest\TestScripts>pytest test_three.py -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\TestScripts
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_three.py::test_task_equality FAILED                                                                                                                             [ 50%]
test_three.py::test_dict_equal FAILED                                                                                                                                [100%]

================================================ FAILURES ===================================================
___________________________________________ test_task_equality ______________________________________________

    def test_task_equality():
        t1 = Task('sit there', 'brain')
        t2 = Task('do something', 'okken')
        print(t1)
        print(t2)
>       assert t1 == t2
E       AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None)
E         At index 0 diff: 'sit there' != 'do something'
E         Full diff:
E         - Task(summary='sit there', owner='brain', done=False, id=None)
E         ?                ^^^  ^^^          ^^^^
E         + Task(summary='do something', owner='okken', done=False, id=None)
E         ?               +++ ^^^  ^^^          ^^^^

test_three.py:14: AssertionError
----------------------------------------- Captured stdout call -----------------------------------------------
Task(summary='sit there', owner='brain', done=False, id=None)
Task(summary='do something', owner='okken', done=False, id=None)
___________________________________________ test_dict_equal __________________________________________________
    def test_dict_equal():
        t1_dict = Task('make sandwich', 'okken')._asdict()
        t2_dict = Task('make sandwich', 'okkem')._asdict()
        print(t1_dict)
        print(t2_dict)
>       assert t1_dict == t2_dict
E       AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)])
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'owner': 'okken'} != {'owner': 'okkem'}
E         Full diff:
E         OrderedDict([('summary', 'make sandwich'),
E         -              ('owner', 'okken'),
E         ?                             ^...
E         
E         ...Full output truncated (5 lines hidden), use '-vv' to show

test_three.py:22: AssertionError
-------------------------------------------- Captured stdout call --------------------------------------------
OrderedDict([('summary', 'make sandwich'), ('owner', 'okken'), ('done', False), ('id', None)])
OrderedDict([('summary', 'make sandwich'), ('owner', 'okkem'), ('done', False), ('id', None)])
========================================== 2 failed in 0.13 seconds ==========================================

预期异常

python 复制代码
"""
在Task项目的API中,有几个地方可能抛出异常
def add(task):  # type:(Task) ->int
def get(task_id):  # type:(int) ->Task
def list_tasks(owner=None):  # type:(str|None) ->list of task
def count():  # type:(None) ->int
def update(task_id, task):  # type:(int, Task) ->None
def delete(task_id):  # type:(int) ->None
def delete_all():  # type:() ->None
def unique_id():  # type:() ->int
def start_tasks_db(db_path, db_type):  # type:(str, str) ->None
def stop_tasks_db():  # type:() ->None
"""

import pytest
import tasks

def test_add_raises():
    with pytest.raises(TypeError):
        tasks.add(task='no a Task object')

"""
测试用例中有with pytest.raise(TypeError)生命,意味着无论with结构中的内容是什么
都至少会发生TypeError异常,如果测试通过,说明确实发生了我们预期TypeError,如果抛出的是其他类型的异常
则与我们预期的异常不一致,测试用例执行失败
"""

def test_start_tasks_db_raises():
    with pytest.raises(ValueError) as excinfo:
        tasks.start_tasks_db('some/great/path', 'mysql')
    exception_msg = excinfo.value.args[0]
    assert exception_msg == "db_type must be a 'tiny' or 'mongo' "
    

测试函数的标记

python 复制代码
from collections import namedtuple
import pytest
Task = namedtuple('Task', ['summary','owner','done','id'])
# __new__.__defaults__创建默认的Task对象
Task.__new__.__defaults__ = (None, None, False, None)


@pytest.mark.smoke
def test_task_equality():
    t1 = Task('sit there', 'brain')
    t2 = Task('do something', 'okken')
    print(t1)
    print(t2)
    assert t1 == t2


@pytest.mark.dict
@pytest.mark.smoke
def test_dict_equal():
    t1_dict = Task('make sandwich', 'okken')._asdict()
    t2_dict = Task('make sandwich', 'okkem')._asdict()
    print(t1_dict)
    print(t2_dict)
    assert t1_dict == t2_dict

执行结果如下:

python 复制代码
E:\Programs\Python\Python_Pytest\TestScripts>pytest -v -m 'smoke' test_five.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
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
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_five.py::test_task_equality FAILED                                                                                                                              [ 50%]
test_five.py::test_dict_equal FAILED                                                                                                                                 [100%]

================================================ FAILURES ====================================================
__________________________________________ test_task_equality ________________________________________________
    @pytest.mark.smoke
    def test_task_equality():
        t1 = Task('sit there', 'brain')
        t2 = Task('do something', 'okken')
        print(t1)
        print(t2)
>       assert t1 == t2
E       AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None)
E         At index 0 diff: 'sit there' != 'do something'
E         Full diff:
E         - Task(summary='sit there', owner='brain', done=False, id=None)
E         ?                ^^^  ^^^          ^^^^
E         + Task(summary='do something', owner='okken', done=False, id=None)
E         ?               +++ ^^^  ^^^          ^^^^

test_five.py:14: AssertionError
--------------------------------------- Captured stdout call -------------------------------------------------
Task(summary='sit there', owner='brain', done=False, id=None)
Task(summary='do something', owner='okken', done=False, id=None)
___________________________________________ test_dict_equal __________________________________________________
    @pytest.mark.dict
    @pytest.mark.smoke
    def test_dict_equal():
        t1_dict = Task('make sandwich', 'okken')._asdict()
        t2_dict = Task('make sandwich', 'okkem')._asdict()
        print(t1_dict)
        print(t2_dict)
>       assert t1_dict == t2_dict
E       AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)])
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'owner': 'okken'} != {'owner': 'okkem'}
E         Full diff:
E         OrderedDict([('summary', 'make sandwich'),
E         -              ('owner', 'okken'),
E         ?                             ^...
E         
E         ...Full output truncated (5 lines hidden), use '-vv' to show

test_five.py:24: AssertionError
--------------------------------------- Captured stdout call -------------------------------------------------
OrderedDict([('summary', 'make sandwich'), ('owner', 'okken'), ('done', False), ('id', None)])
OrderedDict([('summary', 'make sandwich'), ('owner', 'okkem'), ('done', False), ('id', None)])
============================================ warnings summary ================================================
c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo?  You can register custom marks to avoi
d this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.dict - is this a typo?  You can register custom marks to avoid
 this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================================== 2 failed, 2 warnings in 0.22 seconds =====================================

在命令行执行使用了 -m marker_name参数,pytest在执行时会自动寻找被标记为marker_name的测试方法去执行,同时-m还支持and、or、not关键字,如下方式:

python 复制代码
E:\Programs\Python\Python_Pytest\TestScripts>pytest -v -m "smoke and not dict" test_five.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
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
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 / 1 deselected / 1 selected                                                                                                                              

test_five.py::test_task_equality FAILED                                                                                                                              [100%]

============================================= FAILURES =======================================================
_______________________________________ test_task_equality ___________________________________________________

    @pytest.mark.smoke
    def test_task_equality():
        t1 = Task('sit there', 'brain')
        t2 = Task('do something', 'okken')
        print(t1)
        print(t2)
>       assert t1 == t2
E       AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None)
E         At index 0 diff: 'sit there' != 'do something'
E         Full diff:
E         - Task(summary='sit there', owner='brain', done=False, id=None)
E         ?                ^^^  ^^^          ^^^^
E         + Task(summary='do something', owner='okken', done=False, id=None)
E         ?               +++ ^^^  ^^^          ^^^^

test_five.py:14: AssertionError
--------------------------------------- Captured stdout call ------------------------------------------------
Task(summary='sit there', owner='brain', done=False, id=None)
Task(summary='do something', owner='okken', done=False, id=None)
========================================= warnings summary ===================================================
c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo?  You can register custom marks to avoi
d this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.dict - is this a typo?  You can register custom marks to avoid
 this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

-- Docs: https://docs.pytest.org/en/latest/warnings.html
============================= 1 failed, 1 deselected, 2 warnings in 0.14 seconds =============================

跳过测试

python 复制代码
from collections import namedtuple
import pytest
Task = namedtuple('Task', ['summary','owner','done','id'])
# __new__.__defaults__创建默认的Task对象
Task.__new__.__defaults__ = (None, None, False, None)


@pytest.mark.smoke
def test_task_equality():
    t1 = Task('sit there', 'brain')
    t2 = Task('do something', 'okken')
    print(t1)
    print(t2)
    assert t1 == t2


@pytest.mark.dict
@pytest.mark.smoke
@pytest.mark.skip(reason="跳过原因你不知道吗?")
def test_dict_equal():
    t1_dict = Task('make sandwich', 'okken')._asdict()
    t2_dict = Task('make sandwich', 'okkem')._asdict()
    print(t1_dict)
    print(t2_dict)
    assert t1_dict == t2_dict
python 复制代码
E:\Programs\Python\Python_Pytest\TestScripts>pytest -v test_five.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
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
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_five.py::test_task_equality FAILED                                                                                                                              [ 50%]
test_five.py::test_dict_equal SKIPPED                                                                                                                                [100%]

============================================== FAILURES ======================================================
_____________________________________________ test_task_equality _____________________________________________

    @pytest.mark.smoke
    def test_task_equality():
        t1 = Task('sit there', 'brain')
        t2 = Task('do something', 'okken')
        print(t1)
        print(t2)
>       assert t1 == t2
E       AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None)
E         At index 0 diff: 'sit there' != 'do something'
E         Full diff:
E         - Task(summary='sit there', owner='brain', done=False, id=None)
E         ?                ^^^  ^^^          ^^^^
E         + Task(summary='do something', owner='okken', done=False, id=None)
E         ?               +++ ^^^  ^^^          ^^^^

test_five.py:14: AssertionError
--------------------------------------- Captured stdout call -------------------------------------------------
Task(summary='sit there', owner='brain', done=False, id=None)
Task(summary='do something', owner='okken', done=False, id=None)
========================================= warnings summary ===================================================
c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo?  You can register custom marks to avoi
d this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.dict - is this a typo?  You can register custom marks to avoid
 this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

-- Docs: https://docs.pytest.org/en/latest/warnings.html
============================= 1 failed, 1 skipped, 2 warnings in 0.21 seconds ================================

从测试结果中我们看到,测试用例test_five.py::test_dict_equal SKIPPED 被跳过了,然而跳过原因并没有如期的展示出来,可以使用参数-rs来展示跳过原因,如下方式:

python 复制代码
E:\Programs\Python\Python_Pytest\TestScripts>pytest -rs test_five.py
==================================== test session starts =====================================================
platform win32 -- Python 3.7.3, pytest-4.5.0, py-1.8.0, pluggy-0.11.0
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
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_five.py Fs                                                                                                                                                      [100%]

========================================== FAILURES ==========================================================
________________________________________ test_task_equality __________________________________________________
    @pytest.mark.smoke
    def test_task_equality():
        t1 = Task('sit there', 'brain')
        t2 = Task('do something', 'okken')
        print(t1)
        print(t2)
>       assert t1 == t2
E       AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None)
E         At index 0 diff: 'sit there' != 'do something'
E         Use -v to get the full diff

test_five.py:14: AssertionError
-------------------------------------------- Captured stdout call --------------------------------------------
Task(summary='sit there', owner='brain', done=False, id=None)
Task(summary='do something', owner='okken', done=False, id=None)
============================================== warnings summary ==============================================
c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.smoke - is this a typo?  You can register custom marks to avoi
d this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

c:\python37\lib\site-packages\_pytest\mark\structures.py:324
  c:\python37\lib\site-packages\_pytest\mark\structures.py:324: PytestUnknownMarkWarning: Unknown pytest.mark.dict - is this a typo?  You can register custom marks to avoid
 this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    PytestUnknownMarkWarning,

-- Docs: https://docs.pytest.org/en/latest/warnings.html
==================================== short test summary info =================================================
SKIPPED [1] test_five.py:17: 跳过原因你不知道吗?
============================== 1 failed, 1 skipped, 2 warnings in 0.14 seconds ===============================

还可以给跳过添加理由,例如当selenium版本是1.0.4的时候跳过

python 复制代码
from collections import namedtuple
import pytest
import selenium
Task = namedtuple('Task', ['summary','owner','done','id'])
# __new__.__defaults__创建默认的Task对象
Task.__new__.__defaults__ = (None, None, False, None)


@pytest.mark.smoke
def test_task_equality():
    t1 = Task('sit there', 'brain')
    t2 = Task('do something', 'okken')
    print(t1)
    print(t2)
    assert t1 == t2


@pytest.mark.dict
@pytest.mark.smoke
@pytest.mark.skipif(selenium.__version__ == '1.0.4', reason="跳过原因你不知道吗?")
def test_dict_equal():
    t1_dict = Task('make sandwich', 'okken')._asdict()
    t2_dict = Task('make sandwich', 'okkem')._asdict()
    print(t1_dict)
    print(t2_dict)
    assert t1_dict == t2_dict

运行测试子集

运行单个目录,只需要将目录作为pytest的参数即可
pytest -v test/func --tb=no
运行单个测试函数,只需要在文件名后添加::符号和函数名
pytest -v test/func/test_five.py::test_dict_equal
运行单个测试类,与运行单个测试函数类似,在文件名后添加::符号和类名
pytest -v test/func/test_five.py::Test_Update
运行单个测试类中的测试方法,在文件名后添加::符号和类名然后再跟::符号和函数名
pytest -v test/func/test_five.py::Test_Update::test_dict_equal

定制fixture

在pytest中的fixture是在测试函数运行前后,由pytest执行的外壳函数,fixture中的代码可以定制,满足多变的测试需求:包括定义传入测试中的数据集、配置测试前系统的初始化状态、为批量测试提供数据源。

python 复制代码
import pytest

@pytest.fixture()
def return_data():
    return 1000

def test_someting(return_data):
    assert return_data == 1000

执行结果如下:

python 复制代码
(venv) E:\Programs\Python\Python_Pytest\TestScripts>pytest -v test_fixture.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
rootdir: E:\Programs\Python\Python_Pytest\TestScripts
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 1 item                                                                                                                                                                                                                       

test_fixture.py::test_someting PASSED                                                                                                                                                                                            [100%]
========================================= 1 passed in 0.10 seconds ===========================================

通过conftest.py共享fixture

fixture可以放在单独的测试文件里,也可以通过在某个公共目录下新建一个conftest.py文件,将fixture放在其中实现多个测试文件共享fixture。

conftest.py是Python模块,但他不能被测试文件导入,pytest视其为本地插件库。

使用fixture执行配置及销毁逻辑

借助fixture的yield实现setup和teardown

python 复制代码
# encoding = utf-8
import pytest
from selenium import webdriver
from time import sleep

driver = webdriver.Chrome()
url = "https://www.baidu.com"

@pytest.fixture(scope='module')
def automation_testing():
    #  执行测试前
    driver.get(url)
    yield  # 测试结束后
    driver.quit()

def test_fixture(automation_testing):
    driver.find_element_by_id("kw").send_keys("davieyang")
    driver.find_element_by_id("su").click()
    sleep(10)

执行结果如下:

python 复制代码
DY@MacBook-Pro TestScripts$pytest -v test_seven.py 
========================================= test session starts ================================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 1 item                                                                                                                                                             

test_seven.py::test_fixture PASSED                                                                                                                                     [100%]

========================================== 1 passed in 20.45s ================================================

使用--setup-show回溯fixture的执行过程

同样的代码,在执行命令中加上参数--setup-show,便可以看到fixture中实际的执行过程,执行结果如下:

python 复制代码
DY@MacBook-Pro TestScripts$pytest --setup-show test_seven.py 
========================================= test session starts ================================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 1 item                                                                                                                                                             

test_seven.py 
        SETUP    F automation_testing
        test_seven.py::test_fixture (fixtures used: automation_testing).
        TEARDOWN F automation_testing
========================================= 1 passed in 14.69s =================================================

使用fixture传递测试数据

fixture非常适合存放测试数据,并且它可以返回任何数据

python 复制代码
# encoding = utf-8
import pytest

@pytest.fixture()
def return_tuple():
    return ('davieyang', '36', {'mail': 'davieyang@qq.com'})

def test_return_tuple(return_tuple):
    assert return_tuple[2]['mail'] == 'davieyang1@qq.com'

执行结果如下:

python 复制代码
DY@MacBook-Pro TestScripts$pytest --setup-show -v test_seven.py
======================================== test session starts ================================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 1 item                                                                                                                                                             

test_seven.py::test_return_tuple 
        SETUP    F return_tuple
        test_seven.py::test_return_tuple (fixtures used: return_tuple)FAILED
        TEARDOWN F return_tuple

============================================ FAILURES ========================================================
____________________________________________ test_return_tuple _______________________________________________

return_tuple = ('davieyang', '36', {'mail': 'davieyang@qq.com'})

    def test_return_tuple(return_tuple):
>       assert return_tuple[2]['mail'] == 'davieyang1@qq.com'
E       AssertionError: assert 'davieyang@qq.com' == 'davieyang1@qq.com'
E         - davieyang@qq.com
E         + davieyang1@qq.com
E         ?          +

test_seven.py:11: AssertionError
========================================= 1 failed in 0.08s ==================================================

假设在fixture函数中出现了异常,那么执行结果中报的会是Error而不是Fail

python 复制代码
DY@MacBook-Pro TestScripts$pytest --setup-show -v test_seven.py
======================================== test session starts =================================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 0 items / 1 errors                                                                                                                                                 

============================================= ERRORS =========================================================
_______________________________________ ERROR collecting test_seven.py _______________________________________

如果是测试函数异常,那么会报Fail而不是Error

指定fixture作用范围

Fixture中包含一个叫scope的可选参数,用于控制fixture执行配置和销毁逻辑的频率,该参数有4个值分别是:function、class、module和session,其中function为默认值。

Scope Description
function 函数级别的Fixture每个测试函数只需要运行一次,配置代码在测试用例运行之前运行,销毁代码在测试用例运行之后运行
class 类级别的Fixture每个测试类只需要运行一次,无论测试类中有多少类方法都可以共享这个fixture
session 会话级别的Fixture每次会话只需要运行一次,一次pytest会话中的所有测试函数、方法都可以共享这个fixture
module 模块级别的Fixture每个模块只需要运行一次,无论模块里有多少测试函数、类方法或者其他fixture都可以共享这个fixture
python 复制代码
import pytest

@pytest.fixture(scope='function')
def func_scope():
    """The function scope fixture"""


@pytest.fixture(scope='module')
def mod_scope():
    """The module scope fixture"""


@pytest.fixture(scope='class')
def class_scope():
    """The class scope fixture"""


@pytest.fixture(scope='session')
def sess_scope():
    """The session scope fixture"""


def test_one(sess_scope, mod_scope, func_scope):
    """
    Test using session, module, and function scope fixtures
    :param sess_scope:
    :param mod_scope:
    :param func_scope:
    :return:
    """


def test_two(sess_scope, mod_scope, func_scope):
    """
    Again!
    :param sess_scope:
    :param mod_scope:
    :param func_scope:
    :return:
    """


@pytest.mark.usefixtures('class_scope')
class TestSomething:
    def test_three(self):
        """
        Test using class scope fixture
        :return:
        """

    def test_four(self):
        """
        Again
        :return:
        """

执行结果如下:

python 复制代码
DY@MacBook-Pro TestScripts$pytest --setup-show test_eight.py
========================================== test session starts ===============================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 4 items                                                                                                                                                            

test_eight.py 
SETUP    S sess_scope
    SETUP    M mod_scope
        SETUP    F func_scope
        test_eight.py::test_one (fixtures used: func_scope, mod_scope, sess_scope).
        TEARDOWN F func_scope
        SETUP    F func_scope
        test_eight.py::test_two (fixtures used: func_scope, mod_scope, sess_scope).
        TEARDOWN F func_scope
      SETUP    C class_scope
        test_eight.py::TestSomething::test_three (fixtures used: class_scope).
        test_eight.py::TestSomething::test_four (fixtures used: class_scope).
      TEARDOWN C class_scope
    TEARDOWN M mod_scope
TEARDOWN S sess_scope
=========================================== 4 passed in 0.07s ================================================

从执行结果中,可以看到不但出现了代表函数级别和会话级别的F和S,还出现了代表类级别和模块级别的C和M

scope参数是在定义fixture时定义的,而不是在调用fixture时定义,使用fixture的测试函数是无法改变fixture作用范围的

fixture只能使用同级别的fixture或者高级别的fixture,比如函数级别的fixture可以使用同级别的fixture,也可以使用类级别、模块级别和会话级别的fixture,反过来则不可以

使用usefixtures指定fixture

在之前的代码示例中都是在测试函数的参数列表中指定fixture,除了这种方法可以让测试函数使用fixture外,还可以使用@pytest.mark.usefixtures('fixture1', 'fixture2')标记测试函数或测试类,虽然这种方式这对测试函数来说意义不大,因为分别放在测试函数的参数中跟为每个函数做这样的标记比起来或许更简单,但是对于测试类来说,使用这种标记的方式就简便了很多。

python 复制代码
@pytest.mark.usefixtures('class_scope')
class TestSomething:
    def test_three(self):
        """
        Test using class scope fixture
        :return:
        """

    def test_four(self):
        """
        Again
        :return:
        """

使用usefixtures和在测试方法中指定fixture参数大体上差不多,但只有后者才能够使用fixture的返回值

为常用fixture添加autouse选项

通过指定autouse=True使作用域内的测试函数都运行该fixture,在需要多次运行但不依赖任何的系统状态或者外部数据的代码配合起来更加便利。

python 复制代码
import pytest
import time


@pytest.fixture(autouse=True, scope='session')
def footer_session_scope():
    """Report the time at the end of a session"""
    yield
    now = time.time()
    print('- -')
    print('finish:{}'.format(time.strftime('%d %b %X', time.localtime(now))))
    print('---------------')


@pytest.fixture(autouse=True)
def footer_function_scope():
    """Report test durations after each function"""
    start = time.time()
    yield
    stop = time.time()
    delta = stop - start
    print('\ntest duration:{:0.3} seconds'.format(delta))


def test_one():
    """Simulate long-ish running test."""
    time.sleep(1)


def test_two():
    """Simulate slightly longer test"""
    time.sleep(1.23)

执行结果如下:

python 复制代码
DY@MacBook-Pro TestScripts$pytest -v -s test_nine.py
======================================== test session starts =================================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 2 items                                                                                                                                                            

test_nine.py::test_one PASSED
test duration:1.01 seconds

test_nine.py::test_two PASSED
test duration:1.24 seconds
- -
finish:15 Sep 17:12:25
---------------

============================================== 2 passed in 2.26s =============================================

为fixture重命名

python 复制代码
import pytest


@pytest.fixture(name='davieyang')
def rename_fixture_as_davieyang():
    """"""
    return 36


def test_rename_fixture(davieyang):
    assert davieyang == 36

执行结果如下:

python 复制代码
DY@MacBook-Pro TestScripts$pytest --setup-show test_ten.py -v
========================================= test session starts ================================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0 -- /Library/Frameworks/Python.framework/Versions/3.6/bin/python3.6
cachedir: .pytest_cache
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 1 item                                                                                                                                                             

test_ten.py::test_rename_fixture 
        SETUP    F davieyang
        test_ten.py::test_rename_fixture (fixtures used: davieyang)PASSED
        TEARDOWN F davieyang

============================================== 1 passed in 0.01s =============================================

如果想找到'davieyang'在哪里定义的,则可以使用pytest的参数 --fixtures

python 复制代码
DY@MacBook-Pro TestScripts$pytest --fixtures test_ten.py 
========================================== test session starts ===============================================
platform darwin -- Python 3.6.5, pytest-5.1.2, py-1.8.0, pluggy-0.13.0
rootdir: /Volumes/Extended/PythonPrograms/Pytest/TestScripts
collected 1 item                                                                                                                                                             
------------------------------- fixtures defined from TestScripts.test_ten -----------------------------------
davieyang
    Return where we renamed fixture
========================================= no tests ran in 0.04s ==============================================

Fixture的参数化

对测试函数进行参数化,可以多次运行的只是该测试函数;对fixture进行参数化,每个使用该fixture的测试函数都可以执行多次

python 复制代码
task_tuple = (Task('sleep',None, False),
              Task('wake', 'brain',False),
              Task('breathe', 'BRAIN', True),
              Task('exercise', 'BrIaN', False))


task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)for t in task_tuple]


def equivalent(t1, t2):
    """Check two tasks for equivalence"""
    return ((t1.summary == t2.summart)and
            (t1.owner == t2.owner)and
            (t1.done == t2.done))


@pytest.fixture(params=task_tuple)
def a_task(request):
    return request.param


def test_dosomething(tasks_db, a_task):
    task_id = tasks.add(a_task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, a_task)
python 复制代码
task_tuple = (Task('sleep',None, False),
              Task('wake', 'brain',False),
              Task('breathe', 'BRAIN', True),
              Task('exercise', 'BrIaN', False))


task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)for t in task_tuple]


def equivalent(t1, t2):
    """Check two tasks for equivalence"""
    return ((t1.summary == t2.summart)and
            (t1.owner == t2.owner)and
            (t1.done == t2.done))


@pytest.fixture(params=task_tuple, ids=task_ids)
def b_task(request):
    return request.param


def test_dosomething(tasks_db, b_task)):
    task_id = tasks.add(b_task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, b_task)
python 复制代码
task_tuple = (Task('sleep',None, False),
              Task('wake', 'brain',False),
              Task('breathe', 'BRAIN', True),
              Task('exercise', 'BrIaN', False))


def id_function(fixture_value):
    t = fixture_value
    return 'Task({},{},{})'.format(t.summary, t.owner, t.done)
    

def equivalent(t1, t2):
    """Check two tasks for equivalence"""
    return ((t1.summary == t2.summart)and
            (t1.owner == t2.owner)and
            (t1.done == t2.done))


@pytest.fixture(params=task_tuple, ids=id_function)
def c_task(request):
    return request.param


def test_dosomething(tasks_db, c_task):
    task_id = tasks.add(c_task)
    t_from_db = tasks.get(task_id)
    assert equivalent(t_from_db, c_task)

编写插件及分享

基于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

安装插件

常规安装

python 复制代码
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

本地安装插件

python 复制代码
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.com/pytest-dev/pytest-cov
pip install git+https://github.com/pytest-dev/pytest-cov@v2.5.1
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项目为例,列举两个例测试,代码如下:

python 复制代码
"""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代码新增:

python 复制代码
@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()

执行结果:

python 复制代码
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新增:

python 复制代码
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')

在此执行:

python 复制代码
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文件:

python 复制代码
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文件,并在文件中写入:

python 复制代码
[pytest]
nice=True

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

python 复制代码
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再次执行一次:

python 复制代码
(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

创建可安装的插件

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

python 复制代码
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文件中代码如下:

python 复制代码
"""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()函数

python 复制代码
"""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.com',
    license='proprietary',
    py_modules=['pytest_nice'],
    install_requires=['pytest'],
    entry_points={'pytest11': ['nice = pytest_nice', ], },
)

实际上setup()函数还有很多用于提供其他信息的参数,有兴趣可以自行研究这个函数。

setup()函数里的所有参数都是标准的,可用于所有python的安装程序,pytest插件的不同之处在于entry_point参数。

python 复制代码
entry_points={'pytest11': ['nice = pytest_nice', ], },

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

python 复制代码
entry_points={'pytest11':['name_of_plugin=myprojectpluginmodule',],},

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

README.rst文件内容如下:

python 复制代码
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

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

python 复制代码
(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

python 复制代码
"""pytester is needed for testing plugins."""
pytest_plugins = 'pytester'

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

python 复制代码
"""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

在该文件中新增测试方法:

python 复制代码
@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',
    ])

命令行执行该用例:

python 复制代码
(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,结果如下:

python 复制代码
(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目录里

然后执行命令:

python 复制代码
pip install --no-index --find-links myplugins pytest-nice

--no-index告诉pip不要查找PyPI
--find-links myplugins告诉pip在myplugins中查找要安装的软件包

如果你更新了插件,则可以使用--upgrade选项更新安装

python 复制代码
pip install --upgrade --no-index --find-links myplugins pytest-nice

通过PyPI发布插件

可以通过官方地址distributing查询更详细的通过PyPI发布插件的方法

除了python官方地址,还有个非常好的分享插件的地方是cookiecutter,cookiecutter

python 复制代码
pip install cookiecutter
cookiecutter https://github.com/pytest-dev/cookiecutter-pytest-plugin
相关推荐
天天要nx13 小时前
D102【python 接口自动化学习】- pytest进阶之fixture用法
python·pytest
程序猿000001号15 小时前
探索Python的pytest库:简化单元测试的艺术
python·单元测试·pytest
星蓝_starblue2 天前
单元测试(C++)——gmock通用测试模版(个人总结)
c++·单元测试·log4j
whynogome2 天前
单元测试使用记录
单元测试
爱学测试的雨果2 天前
分布式测试插件 pytest-xdist 使用详解
分布式·pytest
字节程序员2 天前
使用JUnit进行集成测试
jmeter·junit·单元测试·集成测试·压力测试
love静思冥想3 天前
Java 单元测试中 JSON 相关的测试案例
java·单元测试·json
七灵微4 天前
【测试】Pytest
pytest
钱钱钱端4 天前
Pytest参数详解 — 基于命令行模式!
自动化测试·软件测试·python·jmeter·职场和发展·pytest·压力测试
乐闻x4 天前
如何使用 TypeScript 和 Jest 编写高质量单元测试
javascript·typescript·单元测试·jest