python第三方测试框架pytest

Pytest vs Unittest

  • 测试用例设计

    • Unitest
      • 测试类必须继承 unittest.TestCase
      • 测试函数必须以 test_ 开头
      • 测试类必须有 unittest.main() 方法
    • Pytest
      • 测试文件必须以 test_ 开头,或者以 _test 结尾
      • 测试类必须以 Test 开头
      • 测试函数必须以 test 开头
      • 测试类里不能使用 "____init" 方法
      • Pytest 兼容 Unittest 测试用例,但 Unittest 不兼容 Pytest
  • 断言比较

    • Unittest
      • assertEqual(a, b) # 判断a和b是否相等
      • assertNotEqual(a, b) # 判断a不等于b
      • assertTrue(a) # 判断a是否为Ture
      • assertFalse(a) #判断a是否为False
      • assertIn(a, b) # a 包含在b里面
      • asserNotIn(a, b) # a 不包含在b里面
    • Pytest
      • assert 后面加需要断言的条件就可以了,例如:
      • assert a = = b # 判断a是否等于b
      • assert a != b # 判断a不等于b、
      • assert a in b # 判断b包含a
  • 用例前置和后置

    • Unittest
      • 通过setup每个用例执行前执行,teardown每个用例执行后执行
      • 通过setupclass类里面所有用例执行前执行,teardownclass类里面所有用例执行后执行
    • Pytest
      • 模块级别:setup_module/teardown_module,整个.py全部用例开始前执行/全部用例执行完后执行
      • 函数级别:setup_function/teardown_function,只对函数级别生效,每个用例开始前和结束后执行一次
      • 类级别:setup_class/teardown_function,只对类级别生效,类里面所有用例开始前执行一次,所有用例执行完执行一次
      • 方法级别:setup_method/teardown_method,只是类里面方法级别生效,方法开始前执行一致,方法结束后执行一次
      • 方法级别:setup/teardown,这个与setup_method/teardown_method用法很类似,但是级别比method级别要低,也就是说在同一个方法中会先执行setup_method再执行setup,方法结束后先执行teardown再执行teardown_method
      • 通过firture可以自定义pytest的前置和后置,格式fixture(scope=”function”, params=None, autouse=False, ids=None, name=None)
        • scope:有四个级别,function(默认),class, module, session
        • params:参数列表
        • autouse:False为默认值,意思代表需要根据设置的条件(scope级别)来激活fixture,如果为Ture,则表示所有function级别的都被激活fixture
        • ids:每个字符串id的列表,感觉没啥实质性作用
        • name:fixture的名字
  • 参数化

    • Unittest:可以通过nose_parameterized来实现,格式:@nose_parameterized.parameterized.expand(data), ‘data’为list格式的参数化的数据
    • Pytest:通过装饰器@pytest.mark.parametrize来实现
  • 生成报告方式

    • Unittest:通过HTMLTestRunner生成
    • Pytest:通过pytest-html生成html格式报告;结合 Allure 可以生成更漂亮的测试报告

环境配置

介绍

pytest 是一个非常成熟的 Python 测试框架,可以做到做个场景的测试工作,如:单元测试、接口测试、web测试等。

  • pytest-selenium(集成 selenium)
  • pytest-html(完美html测试报告生成)
  • pytest-rerunfailures(失败 case 重复执行)
  • pytest-xdist(多CPU分发)
  • 测试用例的 skip 和 xfail 处理
  • 可以很好的和 jenkins 集成

pytest 是一个插件化平台,这就是它比 unittest 强大的原因,丰富的插件扩展增强了它的功能,也可以根据自己的需要定制化开发自己的插件,非常的灵活。

安装

# 安装 pytest 
# !pip3 install pytest
Collecting pytest
  Downloading http://mirrors.tencentyun.com/pypi/packages/c7/e2/c19c667f42f72716a7d03e8dd4d6f63f47d39feadd44cc1ee7ca3089862c/pytest-5.4.1-py3-none-any.whl (246kB)
    100% |████████████████████████████████| 256kB 1.6MB/s ta 0:00:01
[?25hCollecting more-itertools>=4.0.0 (from pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/72/96/4297306cc270eef1e3461da034a3bebe7c84eff052326b130824e98fc3fb/more_itertools-8.2.0-py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 48.1MB/s ta 0:00:01
[?25hCollecting packaging (from pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/62/0a/34641d2bf5c917c96db0ded85ae4da25b6cd922d6b794648d4e7e07c88e5/packaging-20.3-py2.py3-none-any.whl
Collecting py>=1.5.0 (from pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/99/8d/21e1767c009211a62a8e3067280bfce76e89c9f876180308515942304d2d/py-1.8.1-py2.py3-none-any.whl (83kB)
    100% |████████████████████████████████| 92kB 2.8MB/s ta 0:00:011
[?25hCollecting pluggy<1.0,>=0.12 (from pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/a0/28/85c7aa31b80d150b772fbe4a229487bc6644da9ccb7e427dd8cc60cb8a62/pluggy-0.13.1-py2.py3-none-any.whl
Collecting attrs>=17.4.0 (from pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/a2/db/4313ab3be961f7a763066401fb77f7748373b6094076ae2bda2806988af6/attrs-19.3.0-py2.py3-none-any.whl
Collecting wcwidth (from pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/f6/d5/1ecdac957e3ea12c1b319fcdee8b6917ffaff8b4644d673c4d72d2f20b49/wcwidth-0.1.9-py2.py3-none-any.whl
Collecting importlib-metadata>=0.12; python_version < "3.8" (from pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/ad/e4/891bfcaf868ccabc619942f27940c77a8a4b45fd8367098955bb7e152fb1/importlib_metadata-1.6.0-py2.py3-none-any.whl
Collecting pyparsing>=2.0.2 (from packaging->pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl (67kB)
    100% |████████████████████████████████| 71kB 2.2MB/s eta 0:00:01
[?25hCollecting six (from packaging->pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata>=0.12; python_version < "3.8"->pytest)
  Downloading http://mirrors.tencentyun.com/pypi/packages/b2/34/bfcb43cc0ba81f527bc4f40ef41ba2ff4080e047acb0586b56b3d017ace4/zipp-3.1.0-py3-none-any.whl
Installing collected packages: more-itertools, pyparsing, six, packaging, py, zipp, importlib-metadata, pluggy, attrs, wcwidth, pytest
Successfully installed attrs-19.3.0 importlib-metadata-1.6.0 more-itertools-8.2.0 packaging-20.3 pluggy-0.13.1 py-1.8.1 pyparsing-2.4.7 pytest-5.4.1 six-1.14.0 wcwidth-0.1.9 zipp-3.1.0
# 查看 pytest 是否安装成功
# !pytest --version
This is pytest version 5.4.1, imported from /home/ubuntu/.local/lib/python3.6/site-packages/pytest/__init__.py
# 显示可用的内置参数
# !pytest --fixtures
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 0 items                                                              
cache
    Return a cache object that can persist state between testing sessions.
    
    cache.get(key, default)
    cache.set(key, value)
    
    Keys must be a ``/`` separated value, where the first part is usually the
    name of your plugin or application to avoid clashes with other cache users.
    
    Values can be any object handled by the json stdlib module.

capsys
    Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
    
    The captured output is made available via ``capsys.readouterr()`` method
    calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``text`` objects.

capsysbinary
    Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
    
    The captured output is made available via ``capsysbinary.readouterr()``
    method calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``bytes`` objects.

capfd
    Enable text capturing of writes to file descriptors ``1`` and ``2``.
    
    The captured output is made available via ``capfd.readouterr()`` method
    calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``text`` objects.

capfdbinary
    Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
    
    The captured output is made available via ``capfd.readouterr()`` method
    calls, which return a ``(out, err)`` namedtuple.
    ``out`` and ``err`` will be ``byte`` objects.

doctest_namespace [session scope]
    Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests.

pytestconfig [session scope]
    Session-scoped fixture that returns the :class:`_pytest.config.Config` object.
    
    Example::
    
        def test_foo(pytestconfig):
            if pytestconfig.getoption("verbose") > 0:
                ...

record_property
    Add an extra properties the calling test.
    User properties become part of the test report and are available to the
    configured reporters, like JUnit XML.
    The fixture is callable with ``(name, value)``, with value being automatically
    xml-encoded.
    
    Example::
    
        def test_function(record_property):
            record_property("example_key", 1)

record_xml_attribute
    Add extra xml attributes to the tag for the calling test.
    The fixture is callable with ``(name, value)``, with value being
    automatically xml-encoded

record_testsuite_property [session scope]
    Records a new ``<property>`` tag as child of the root ``<testsuite>``. This is suitable to
    writing global information regarding the entire test suite, and is compatible with ``xunit2`` JUnit family.
    
    This is a ``session``-scoped fixture which is called with ``(name, value)``. Example:
    
    .. code-block:: python
    
        def test_foo(record_testsuite_property):
            record_testsuite_property("ARCH", "PPC")
            record_testsuite_property("STORAGE_TYPE", "CEPH")
    
    ``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.

caplog
    Access and control log capturing.
    
    Captured logs are available through the following properties/methods::
    
    * caplog.messages        -> list of format-interpolated log messages
    * caplog.text            -> string containing formatted log output
    * caplog.records         -> list of logging.LogRecord instances
    * caplog.record_tuples   -> list of (logger_name, level, message) tuples
    * caplog.clear()         -> clear captured records and formatted log output string

monkeypatch
    The returned ``monkeypatch`` fixture provides these
    helper methods to modify objects, dictionaries or os.environ::
    
        monkeypatch.setattr(obj, name, value, raising=True)
        monkeypatch.delattr(obj, name, raising=True)
        monkeypatch.setitem(mapping, name, value)
        monkeypatch.delitem(obj, name, raising=True)
        monkeypatch.setenv(name, value, prepend=False)
        monkeypatch.delenv(name, raising=True)
        monkeypatch.syspath_prepend(path)
        monkeypatch.chdir(path)
    
    All modifications will be undone after the requesting
    test function or fixture has finished. The ``raising``
    parameter determines if a KeyError or AttributeError
    will be raised if the set/deletion operation has no target.

recwarn
    Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
    
    See http://docs.python.org/library/warnings.html for information
    on warning categories.

tmpdir_factory [session scope]
    Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session.
        

tmp_path_factory [session scope]
    Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session.
        

tmpdir
    Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a `py.path.local`_
    path object.
    
    .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html

tmp_path
    Return a temporary directory path object
    which is unique to each test function invocation,
    created as a sub directory of the base temporary
    directory.  The returned object is a :class:`pathlib.Path`
    object.
    
    .. note::
    
        in python < 3.6 this is a pathlib2.Path


============================ no tests ran in 0.03s =============================

执行用例

执行退出码

0 -- 成功地收集并传递了所有测试
1 -- 测试被收集和运行但一些测试失败

2 -- 测试执行被用户中断
3 -- 执行测试时发生内部错误
4 -- pytest 命令行使用错误
5 -- 未收集任何测试

执行测试文件

"""
示例文件
@FileName: test_start.py
"""

def func(x):
    return x + 1

def test_func():
    assert func(3) == 5
    
class TestClass:
    def test_one(self):
        x = "This"
        assert "h" in x
        
    def test_two(self):
        x = "hello"
        assert hasattr(x, "check")

执行某个目录下所有的用例

!pytest 
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

test_start.py F.F                                                        [100%]

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_start.py:8: AssertionError
______________________________ TestClass.test_two ______________________________

self = <test_start.TestClass object at 0x7f384557fc18>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

test_start.py:17: AssertionError
=========================== short test summary info ============================
FAILED test_start.py::test_func - assert 4 == 5
FAILED test_start.py::TestClass::test_two - AssertionError: assert False
========================= 2 failed, 1 passed in 0.12s ==========================

执行某个 py 文件下用例

!pytest start.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

start.py F.F                                                             [100%]

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

start.py:8: AssertionError
______________________________ TestClass.test_two ______________________________

self = <start.TestClass object at 0x7f0d00651278>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

start.py:17: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
FAILED start.py::TestClass::test_two - AssertionError: assert False
========================= 2 failed, 1 passed in 0.12s ==========================

执行 py 文件中的某个函数

!pytest start.py::test_func
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

start.py F                                                               [100%]

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

start.py:8: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
============================== 1 failed in 0.10s ===============================

执行 py 文件中的某个类

!pytest start.py::TestClass
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 2 items                                                              

start.py .F                                                              [100%]

=================================== FAILURES ===================================
______________________________ TestClass.test_two ______________________________

self = <start.TestClass object at 0x7f5fc75cb160>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

start.py:17: AssertionError
=========================== short test summary info ============================
FAILED start.py::TestClass::test_two - AssertionError: assert False
========================= 1 failed, 1 passed in 0.13s ==========================

执行 py 文件中类里的某个方法

!pytest start.py::TestClass::test_one
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

start.py .                                                               [100%]

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

-v 显示每个测试函数结果

"""显示每一个测试函数的执行结果"""
!pytest -v start.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

start.py::test_func FAILED                                               [ 33%]
start.py::TestClass::test_one PASSED                                     [ 66%]
start.py::TestClass::test_two FAILED                                     [100%]

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E         +4
E         -5

start.py:8: AssertionError
______________________________ TestClass.test_two ______________________________

self = <start.TestClass object at 0x7fc060a955c0>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

start.py:17: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
FAILED start.py::TestClass::test_two - AssertionError: assert False
========================= 2 failed, 1 passed in 0.16s ==========================

-m 标记表达式

# 将运行用 @pytest.mark.login 装饰器修饰的所有测试
!pytest -m login 
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 4 items / 4 deselected                                               

============================ 4 deselected in 0.04s =============================

-q 简单但因,只打印测试用例的执行结果

!pytest -q start.py
F.F                                                                      [100%]
=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

start.py:8: AssertionError
______________________________ TestClass.test_two ______________________________

self = <start.TestClass object at 0x7fa6e310a4a8>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

start.py:17: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
FAILED start.py::TestClass::test_two - AssertionError: assert False
2 failed, 1 passed in 0.16s

-s 详细打印

!pytest -s start.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

start.py F.F

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

start.py:8: AssertionError
______________________________ TestClass.test_two ______________________________

self = <start.TestClass object at 0x7f087090bd30>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

start.py:17: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
FAILED start.py::TestClass::test_two - AssertionError: assert False
========================= 2 failed, 1 passed in 0.12s ==========================

-x 遇到错误时停止测试

!pytest start.py -x
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

start.py F

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

start.py:8: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
============================== 1 failed in 0.11s ===============================

--maxfail=num,当用例错误个数达到指定数量是,停止测试

!pytest start.py --maxfail=1
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

start.py F

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

start.py:8: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
============================== 1 failed in 0.12s ===============================

-k 匹配用例名称

# 执行测试用例名称包含http的所有用例
!pytest -s -k http start.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items / 3 deselected                                               

============================ 3 deselected in 0.01s =============================

-k 根据用例名称排除某些用例

!pytest -s -k "not http" start.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

start.py F.F

=================================== FAILURES ===================================
__________________________________ test_func ___________________________________

    def test_func():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

start.py:8: AssertionError
______________________________ TestClass.test_two ______________________________

self = <start.TestClass object at 0x7f946a3cfe80>

    def test_two(self):
        x = "hello"
>       assert hasattr(x, "check")
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

start.py:17: AssertionError
=========================== short test summary info ============================
FAILED start.py::test_func - assert 4 == 5
FAILED start.py::TestClass::test_two - AssertionError: assert False
========================= 2 failed, 1 passed in 0.12s ==========================

-k 同时匹配不同的用例名称

!pytest -s -k "method or weibo" start.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items / 3 deselected                                               

============================ 3 deselected in 0.01s =============================

断言 assert

常用断言

  • 与unittest不同,pytest使用的是python自带的assert关键字来进行断言

  • assert关键字后面可以接一个表达式,只要表达式的最终结果为True,那么断言通过,用例执行成功,否则用例执行失败

常用断言方法:

  • assert xx --- 判断 xx 为真
  • assert not xx --- 判断 xx 不为真
  • assert a in b --- 判断 b 包含 a
  • assert a == b --- 判断 a 等于 b
  • assert a != b --- 判断 a 不等于 b
import pytest

def f():
    return 3

def test_function():
    a = f()
    assert a % 2 == 0, "判断 a 为偶数,当前 a 的值为:%s" % a
    
def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0
        
!pytest assert.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 2 items                                                              

assert.py F.                                                             [100%]

=================================== FAILURES ===================================
________________________________ test_function _________________________________

    def test_function():
        a = f()
>       assert a % 2 == 0, "判断 a 为偶数,当前 a 的值为:%s" % a
E       AssertionError: 判断 a 为偶数,当前 a 的值为:3
E       assert (3 % 2) == 0

assert.py:8: AssertionError
=========================== short test summary info ============================
FAILED assert.py::test_function - AssertionError: 判断 a 为偶数,当前 a 的值...
========================= 1 failed, 1 passed in 0.11s ==========================

异常断言

可以使用 pytest.raises 作为上下文管理器,当抛出异常时可以获取到对应的异常实例

  • excinfo :是一个异常信息实例
  • 主要属性: .type 、 .value 、 .traceback
  • 注意:断言 type 的时候,异常类型是不需要加引号的,断言 value值的时候需转 str
"""异常断言"""
import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0
  
"""详细断言异常"""
def test_zero_division_long_info():
    with pytest.raises(ZeroDivisionError) as excinfo:
        1 / 0
        
    """断言异常类型"""
    assert excinfo.type == ZeroDivisionError
    
    """断言异常 value 的值"""
    assert "Division by Zero" in str(excinfo.value)
        

!pytest assert_except.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 2 items                                                              

assert_except.py .F                                                      [100%]

=================================== FAILURES ===================================
_________________________ test_zero_division_long_info _________________________

    def test_zero_division_long_info():
        with pytest.raises(ZeroDivisionError) as excinfo:
            1 / 0
    
        """断言异常类型"""
        assert excinfo.type == ZeroDivisionError
    
        """断言异常 value 的值"""
>       assert "Division by Zero" in str(excinfo.value)
E       AssertionError: assert 'Division by Zero' in 'division by zero'
E        +  where 'division by zero' = str(ZeroDivisionError('division by zero',))
E        +    where ZeroDivisionError('division by zero',) = <ExceptionInfo ZeroDivisionError('division by zero',) tblen=1>.value

assert_except.py:17: AssertionError
=========================== short test summary info ============================
FAILED assert_except.py::test_zero_division_long_info - AssertionError: asser...
========================= 1 failed, 1 passed in 0.11s ==========================

match

  • 可以将 match 关键字参数传递给上下文管理器,以测试正则表达式与异常的字符串表示形式是否匹配
  • 注意:这种方法只能断言value,不能断言type
"""自定义消息"""
def test_zero_division_custom_incinfo():
    with pytest.raises(ZeroDivisionError, match="*.zero.*") as excinfo:
        1 / 0
        

!pytest assert_except.py::test_zero_division_custom_incinfo
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

assert_except.py F                                                       [100%]

=================================== FAILURES ===================================
______________________ test_zero_division_custom_incinfo _______________________

    def test_zero_division_custom_incinfo():
        with pytest.raises(ZeroDivisionError, match="*.zero.*") as excinfo:
>           1 / 0
E           ZeroDivisionError: division by zero

assert_except.py:23: ZeroDivisionError

During handling of the above exception, another exception occurred:

    def test_zero_division_custom_incinfo():
        with pytest.raises(ZeroDivisionError, match="*.zero.*") as excinfo:
>           1 / 0

assert_except.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/lib/python3.6/re.py:182: in search
    return _compile(pattern, flags).search(string)
/usr/lib/python3.6/re.py:301: in _compile
    p = sre_compile.compile(pattern, flags)
/usr/lib/python3.6/sre_compile.py:562: in compile
    p = sre_parse.parse(p, flags)
/usr/lib/python3.6/sre_parse.py:855: in parse
    p = _parse_sub(source, pattern, flags & SRE_FLAG_VERBOSE, 0)
/usr/lib/python3.6/sre_parse.py:416: in _parse_sub
    not nested and not items))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

source = <sre_parse.Tokenizer object at 0x7f7aac6d9630>
state = <sre_parse.Pattern object at 0x7f7aac663390>, verbose = 0, nested = 1
first = True

    def _parse(source, state, verbose, nested, first=False):
        # parse a simple pattern
        subpattern = SubPattern(state)
    
        # precompute constants into local variables
        subpatternappend = subpattern.append
        sourceget = source.get
        sourcematch = source.match
        _len = len
        _ord = ord
    
        while True:
    
            this = source.next
            if this is None:
                break # end of pattern
            if this in "|)":
                break # end of subpattern
            sourceget()
    
            if verbose:
                # skip whitespace and comments
                if this in WHITESPACE:
                    continue
                if this == "#":
                    while True:
                        this = sourceget()
                        if this is None or this == "\n":
                            break
                    continue
    
            if this[0] == "\\":
                code = _escape(source, this, state)
                subpatternappend(code)
    
            elif this not in SPECIAL_CHARS:
                subpatternappend((LITERAL, _ord(this)))
    
            elif this == "[":
                here = source.tell() - 1
                # character set
                set = []
                setappend = set.append
    ##          if sourcematch(":"):
    ##              pass # handle character classes
                if sourcematch("^"):
                    setappend((NEGATE, None))
                # check remaining characters
                start = set[:]
                while True:
                    this = sourceget()
                    if this is None:
                        raise source.error("unterminated character set",
                                           source.tell() - here)
                    if this == "]" and set != start:
                        break
                    elif this[0] == "\\":
                        code1 = _class_escape(source, this)
                    else:
                        code1 = LITERAL, _ord(this)
                    if sourcematch("-"):
                        # potential range
                        that = sourceget()
                        if that is None:
                            raise source.error("unterminated character set",
                                               source.tell() - here)
                        if that == "]":
                            if code1[0] is IN:
                                code1 = code1[1][0]
                            setappend(code1)
                            setappend((LITERAL, _ord("-")))
                            break
                        if that[0] == "\\":
                            code2 = _class_escape(source, that)
                        else:
                            code2 = LITERAL, _ord(that)
                        if code1[0] != LITERAL or code2[0] != LITERAL:
                            msg = "bad character range %s-%s" % (this, that)
                            raise source.error(msg, len(this) + 1 + len(that))
                        lo = code1[1]
                        hi = code2[1]
                        if hi < lo:
                            msg = "bad character range %s-%s" % (this, that)
                            raise source.error(msg, len(this) + 1 + len(that))
                        setappend((RANGE, (lo, hi)))
                    else:
                        if code1[0] is IN:
                            code1 = code1[1][0]
                        setappend(code1)
    
                # XXX: <fl> should move set optimization to compiler!
                if _len(set)==1 and set[0][0] is LITERAL:
                    subpatternappend(set[0]) # optimization
                elif _len(set)==2 and set[0][0] is NEGATE and set[1][0] is LITERAL:
                    subpatternappend((NOT_LITERAL, set[1][1])) # optimization
                else:
                    # XXX: <fl> should add charmap optimization here
                    subpatternappend((IN, set))
    
            elif this in REPEAT_CHARS:
                # repeat previous item
                here = source.tell()
                if this == "?":
                    min, max = 0, 1
                elif this == "*":
                    min, max = 0, MAXREPEAT
    
                elif this == "+":
                    min, max = 1, MAXREPEAT
                elif this == "{":
                    if source.next == "}":
                        subpatternappend((LITERAL, _ord(this)))
                        continue
                    min, max = 0, MAXREPEAT
                    lo = hi = ""
                    while source.next in DIGITS:
                        lo += sourceget()
                    if sourcematch(","):
                        while source.next in DIGITS:
                            hi += sourceget()
                    else:
                        hi = lo
                    if not sourcematch("}"):
                        subpatternappend((LITERAL, _ord(this)))
                        source.seek(here)
                        continue
                    if lo:
                        min = int(lo)
                        if min >= MAXREPEAT:
                            raise OverflowError("the repetition number is too large")
                    if hi:
                        max = int(hi)
                        if max >= MAXREPEAT:
                            raise OverflowError("the repetition number is too large")
                        if max < min:
                            raise source.error("min repeat greater than max repeat",
                                               source.tell() - here)
                else:
                    raise AssertionError("unsupported quantifier %r" % (char,))
                # figure out which item to repeat
                if subpattern:
                    item = subpattern[-1:]
                else:
                    item = None
                if not item or (_len(item) == 1 and item[0][0] is AT):
                    raise source.error("nothing to repeat",
>                                      source.tell() - here + len(this))
E                   sre_constants.error: nothing to repeat at position 0

/usr/lib/python3.6/sre_parse.py:616: error
=========================== short test summary info ============================
FAILED assert_except.py::test_zero_division_custom_incinfo - sre_constants.er...
============================== 1 failed in 0.26s ===============================

检查断言装饰器

  • 代码抛出异常,但是和raises指定的异常类相匹配,所以不会断言失败
  • 它相当于一个检查异常装饰器,功能:检查是否有异常,不确定是否有异常
  • with pytest.raise(ZeroDivisionError) 对于故意测试异常代码的情况,使用可能会更好
  • 而@pytest.mark.xfail(raises=ZeroDivisionError) 对于检查未修复的错误(即,可能会发生异常),使用检查断言可能会更好
"""异常装饰器"""
@pytest.mark.xfail(raises=ZeroDivisionError)
def test_except_decorate():
    1 / 0
    

!pytest assert_except.py::test_except_decorate
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

assert_except.py x                                                       [100%]

============================== 1 xfailed in 0.03s ==============================

setup 和 teardown

  • unittest的前置后置

    • 每个测试方法前后都会执行:setup() teardown()
    • 代码运行前后执行:setupClass() teardownClass()
  • pytest的前置后置

    • 模块级别:setup_module、teardown_module
    • 函数级别:setup_function、teardown_function,不在类中的方法
    • 类级别:setup_class、teardown_class
    • 方法级别:setup_method、teardown_method
    • 方法细化级别:setup、teardown

注意:setup、teardown可以实现在执行用例前或结束后加入一些操作,但这种都是针对整个脚本全局生效的

"""前置后置条件"""

import pytest

def setup_module():
    print("=====整个.py模块开始前只执行一次:打开浏览器=====")
    
def teardown_module():
    print("=====整个.py模块结束后只执行一次:关闭浏览器=====")
 

def setup_function():
    print("===每个函数级别用例开始前都执行setup_function===")

def teardown_function():
    print("===每个函数级别用例结束后都执行teardown_function====")
    
    
def test_one():
    print("one")

def test_two():
    print("two")
    
    
class TestCase():
    def setup_class(self):
        print("====整个测试类开始前只执行一次setup_class====")

    def teardown_class(self):
        print("====整个测试类结束后只执行一次teardown_class====")

    def setup_method(self):
        print("==类里面每个用例执行前都会执行setup_method==")

    def teardown_method(self):
        print("==类里面每个用例结束后都会执行teardown_method==")

    def setup(self):
        print("=类里面每个用例执行前都会执行setup=")

    def teardown(self):
        print("=类里面每个用例结束后都会执行teardown=")

    def test_three(self):
        print("three")
        
def test_four(self):
        print("four")
        

if __name__ == '__main__':
    pytest.main(["-q", "-s", "-ra", "setup_teardown.py"])
    
    
!pytest setup_teardown.py
=====整个.py模块开始前只执行一次:打开浏览器=====
===每个函数级别用例开始前都执行setup_function===
one
.===每个函数级别用例结束后都执行teardown_function====
===每个函数级别用例开始前都执行setup_function===
two
.===每个函数级别用例结束后都执行teardown_function====
====整个测试类开始前只执行一次setup_class====
==类里面每个用例执行前都会执行setup_method==
=类里面每个用例执行前都会执行setup=
three
.=类里面每个用例结束后都会执行teardown=
==类里面每个用例结束后都会执行teardown_method==
====整个测试类结束后只执行一次teardown_class====
===每个函数级别用例开始前都执行setup_function===
E===每个函数级别用例结束后都执行teardown_function====
=====整个.py模块结束后只执行一次:关闭浏览器=====

==================================== ERRORS ====================================
_________________________ ERROR at setup of test_four __________________________
file /home/ubuntu/MySpace/Python/pytest/setup_teardown.py, line 48
  def test_four(self):
E       fixture 'self' not found
>       available fixtures: _Module__pytest_setup_function, _Module__pytest_setup_module, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/setup_teardown.py:48
=========================== short test summary info ============================
ERROR setup_teardown.py::test_four
3 passed, 1 error in 0.05s
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 4 items                                                              

setup_teardown.py ...E                                                   [100%]

==================================== ERRORS ====================================
_________________________ ERROR at setup of test_four __________________________
file /home/ubuntu/MySpace/Python/pytest/setup_teardown.py, line 48
  def test_four(self):
E       fixture 'self' not found
>       available fixtures: _Module__pytest_setup_function, _Module__pytest_setup_module, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/setup_teardown.py:48
---------------------------- Captured stdout setup -----------------------------
===每个函数级别用例开始前都执行setup_function===
=========================== short test summary info ============================
ERROR setup_teardown.py::test_four
========================== 3 passed, 1 error in 0.04s ==========================

fixture

如果有以下场景:用例 1 需要先登录,用例 2 不需要登录,用例 3 需要先登录。很显然无法用 setup 和 teardown 来实现了

fixture可以让我们自定义测试用例的前置条件

fixture基础

fixture的优势

  • 命名方式灵活,不局限于 setupteardown 这几个命名
  • conftest.py 配置里可以实现数据共享,不需要 import 就能自动找到 fixture
  • scope="module" 可以实现多个 .py 跨文件共享前置
  • scope="session" 以实现多个 .py 跨文件使用一个 session 来完成多个用例

fixture参数列表

@pytest.fixture(scope="function", params=None, autouse=False, ids=None, name=None)
def test():
    print("fixture初始化的参数列表")
  • 参数列表

    • scope:可以理解成fixture的作用域,默认:function,还有class、module、package、session四个【常用】
    • autouse:默认:False,需要用例手动调用该fixture;如果是True,所有作用域内的测试用例都会自动调用该fixture
    • name:默认:装饰器的名称,同一模块的fixture相互调用建议写个不同的name
  • 注意

    • session的作用域:是整个测试会话,即开始执行pytest到结束测试

调用 fixture

方法一:将fixture名称作为测试用例函数的输入参数

import pytest

@pytest.fixture
def login():
    print("请输入帐号和密码")
    
def test_s1(login):
    print("用例1,登录后进行的,操作 111")
    
"""不穿login"""
def test_s2():
    print("用例2,不需要登录,操作 222")
    
    
!pytest fixture.py::test_s1
!pytest fixture.py::test_s2
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

fixture.py .                                                             [100%]

============================== 1 passed in 0.01s ===============================
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

fixture.py .                                                             [100%]

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

方法二:测试用例加上装饰器:@pytest.mark.usefixtures(fixture_name)

import pytest

@pytest.fixture
def login():
    print("请输入帐号和密码")

@pytest.fixture
def login2():
    print("please输入账号,密码先登录")


@pytest.mark.usefixtures("login2", "login")
def test_s11():
    print("用例 11:登录之后其它动作 111")

!pytest fixture.py::test_s11
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

fixture.py .                                                             [100%]

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

方法三:fixture设置autouse=True

  • 在类声明上面加 @pytest.mark.usefixtures() ,代表这个类里面所有测试用例都会调用该fixture
  • 可以叠加多个 @pytest.mark.usefixtures() ,先执行的放底层,后执行的放上层
  • 可以传多个fixture参数,先执行的放前面,后执行的放后面
  • 如果fixture有返回值,用 @pytest.mark.usefixtures() 是无法获取到返回值的,必须用传参的方式
@pytest.fixture(autouse=True)
def login3():
    print("====auto===")


"""不是test开头,加了装饰器也不会执行fixture"""
@pytest.mark.usefixtures("login2")
def test_loginss():
    print(123)
    
    
!pytest fixture.py::test_loginss
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

fixture.py .                                                             [100%]

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

fixture实例化顺序

  • 实例化【session > package > module > class > function】
  • 具有相同作用域的fixture遵循测试函数中声明的顺序,并遵循fixture之间的依赖关系【在fixture_A里面依赖的fixture_B优先实例化,然后到fixture_A实例化】
  • 自动使用(autouse=True)的fixture将在显式使用(传参或装饰器)的fixture之前实例化
import pytest

order = []

@pytest.fixture(scope="session")
def s1():
    order.append("s1")


@pytest.fixture(scope="module")
def m1():
    order.append("m1")
    
@pytest.fixture
def f1(f3, a1):
    # 先实例化f3, 再实例化a1, 最后实例化f1
    order.append("f1")
    assert f3 == 123


@pytest.fixture
def f3():
    order.append("f3")
    a = 123
    yield a


@pytest.fixture
def a1():
    order.append("a1")


@pytest.fixture
def f2():
    order.append("f2")


def test_order(f1, m1, f2, s1):
    # m1、s1在f1后,但因为scope范围大,所以会优先实例化
    assert order == ["s1", "m1", "f3", "a1", "f1", "f2"]
    
    
!pytest fixture_order.py::test_order
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

fixture_order.py .                                                       [100%]

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

fixture注意点

添加了 @pytest.fixture ,如果fixture还想依赖其他fixture,需要用函数传参的方式,不能用 @pytest.mark.usefixtures() 的方式,否则会不生效

import pytest

@pytest.fixture(scope="session")
def open():
    print("===打开浏览器===")

@pytest.fixture
# @pytest.mark.usefixtures("open") 不可取!!!不生效!!!
def test_login(open):
    # 方法级别前置操作setup
    print(f"输入账号,密码先登录{open}")
    
    
!pytest fixture_attention.py::test_login
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 0 items                                                              

============================ no tests ran in 0.01s =============================
ERROR: not found: /home/ubuntu/MySpace/Python/pytest/fixture_attention.py::test_login
(no name '/home/ubuntu/MySpace/Python/pytest/fixture_attention.py::test_login' in any of [<Module fixture_attention.py>])


fixture实现前置和后置

fixture之yield实现teardown

用fixture实现teardown并不是一个独立的函数,而是用 yield 关键字来开启teardown操作

  • yield注意事项
    • 如果yield前面的代码,即setup部分已经抛出异常了,则不会执行yield后面的teardown内容
    • 如果测试用例抛出异常,yield后面的teardown内容还是会正常执行
import pytest


@pytest.fixture(scope="session")
def open():
    # 会话前置操作setup
    print("===打开浏览器===")
    test = "测试变量是否返回"
    yield test
    # 会话后置操作teardown
    print("==关闭浏览器==")


@pytest.fixture
def login(open):
    # 方法级别前置操作setup
    print(f"输入账号,密码先登录{open}")
    name = "==我是账号=="
    pwd = "==我是密码=="
    age = "==我是年龄=="
    # 返回变量
    yield name, pwd, age
    # 方法级别后置操作teardown
    print("登录成功")


def test_s1(login):
    print("==用例1==")
    # 返回的是一个元组
    print(login)
    # 分别赋值给不同变量
    name, pwd, age = login
    print(name, pwd, age)
    assert "账号" in name
    assert "密码" in pwd
    assert "年龄" in age


def test_s2(login):
    print("==用例2==")
    print(login)
    
    
!pytest fixture_teardown.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 2 items                                                              

fixture_teardown.py ..                                                   [100%]

============================== 2 passed in 0.01s ===============================

with和yeild

import pytest

@pytest.fixture(scope="module")
def smtp_connection():
    with smtp.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection:
        yield smtp_connection  # provide the fixture value
        
        
!pytest fixture_yeild_with.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 0 items                                                              

============================ no tests ran in 0.00s =============================

addfinalizer函数

  • 如果 request.addfinalizer() 前面的代码,即setup部分已经抛出异常了,则不会执行 request.addfinalizer() 的teardown内容(和yield相似,应该是最近新版本改成一致了)
  • 可以声明多个终结函数并调用
@pytest.fixture(scope="module")
def test_addfinalizer(request):
    # 前置操作setup
    print("==再次打开浏览器==")
    test = "test_addfinalizer"

    def fin():
        # 后置操作teardown
        print("==再次关闭浏览器==")

    request.addfinalizer(fin)
    # 返回前置操作的变量
    return test


def test_anthor(test_addfinalizer):
    print("==最新用例==", test_addfinalizer)

conftest.py

  • conftest.py --- 一个专门存放 fixture 的配置文件,多个测试用例文件(test_*.py)的所有用例都需要用登录功能来作为前置操作,那就不能把登录功能写到某个用例文件中去了

  • conftest.py的出现,就是为了解决上述问题,单独管理一些全局的fixture

  • conftest.py配置fixture注意事项

    • pytest会默认读取conftest.py里面的所有fixture
    • conftest.py 文件名称是固定的,不能改动
    • conftest.py只对同一个package下的所有测试用例生效
    • 不同目录可以有自己的conftest.py,一个项目中可以有多个conftest.py
    • 测试用例文件中不需要手动import conftest.py,pytest会自动查找
!tree ./conftest/

!pytest ./conftest/run.py
./conftest/
├── confest.py
├── __pycache__
│   └── test_1.cpython-36-pytest-5.4.1.pyc
├── run.py
└── test_1.py

1 directory, 4 files
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 0 items                                                              

============================ no tests ran in 0.00s =============================

fixture传参request

  • 为了提高复用性,我们在写测试用例的时候,会用到不同的fixture,比如:最常见的登录操作,大部分的用例的前置条件都是登录
  • 假设不同的用例想登录不同的测试账号,那么登录fixture就不能把账号写死,需要通过传参的方式来完成登录操作

一个参数

import pytest


@pytest.fixture()
def login(request):
    name = request.param
    print(f"== 账号是:{name} ==")
    return name


data = ["pyy1", "polo"]
ids = [f"login_test_name is:{name}" for name in data]

"""
(1)添加  indirect=True  参数是为了把 login 当成一个函数去执行,而不是一个参数,并且将data当做参数传入函数
(2)def test_name(login) ,这里的login是获取fixture返回的值
"""
@pytest.mark.parametrize("login", data, ids=ids, indirect=True)
def test_name(login):
    print(f" 测试用例的登录账号是:{login} ")
    
    
!pytest -s fixture/fixture_request_one.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 2 items                                                              

fixture/fixture_request_one.py == 账号是:pyy1 ==
 测试用例的登录账号是:pyy1 
.== 账号是:polo ==
 测试用例的登录账号是:polo 
.

============================== 2 passed in 0.01s ===============================

多个参数

需要传多个参数,需要通过字典去传

"""多个参数"""
import pytest


@pytest.fixture()
def login(request):
    param = request.param
    print(f"账号是:{param['username']}, 密码是:{param['passwd']}")
    return param

data = [
    {"username": "crisimple1", "passwd": "123456"},
    {"username": "crisimple2", "passwd": "654321"},
]

@pytest.mark.parametrize("login", data, indirect=True)
def test_login(login):
    print(f"账号是:{login['username']},密码是:{login['passwd']}")
    
    
!pytest -v -s fixture/fixture_request_more.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 2 items                                                              

fixture/fixture_request_more.py::test_login[login0] 账号是:crisimple1, 密码是:123456
账号是:crisimple1,密码是:123456
PASSED
fixture/fixture_request_more.py::test_login[login1] 账号是:crisimple2, 密码是:654321
账号是:crisimple2,密码是:654321
PASSED

============================== 2 passed in 0.01s ===============================

多个fixture(只加一个装饰器)

"""多个fixture,只加一个装饰器"""
import pytest

@pytest.fixture(scope="function")
def input_user(request):
    user = request.param
    print("登录账号为:%s" % user)
    return user

@pytest.fixture(scope="function")
def input_pwd(request):
    pwd = request.param
    print("登录密码为:%s" % pwd)
    return pwd

data = [
    ("name1", "pwd1"),
    ("name2", "pwd2")
]

@pytest.mark.parametrize("input_user, input_pwd", data, indirect=True)
def test_more_fixture(input_user, input_pwd):
    print("fixture返回的内容:", input_user, input_pwd)
    

!pytest -v -s fixture/fixture_one_fixture.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 2 items                                                              

fixture/fixture_one_fixture.py::test_more_fixture[name1-pwd1] 登录账号为:name1
登录密码为:pwd1
fixture返回的内容: name1 pwd1
PASSED
fixture/fixture_one_fixture.py::test_more_fixture[name2-pwd2] 登录账号为:name2
登录密码为:pwd2
fixture返回的内容: name2 pwd2
PASSED

============================== 2 passed in 0.01s ===============================

多个fixture(叠加装饰器)

"""多个fixture"""
import pytest

@pytest.fixture(scope="function")
def input_user(request):
    user = request.param
    print("登录账号为:%s" % user)
    return user

@pytest.fixture(scope="function")
def input_pwd(request):
    pwd = request.param
    print("登录密码为:%s" % pwd)
    return pwd

name = ["name1", "name2"]
passwd = ["pwd1", "pwd2"]

@pytest.mark.parametrize("input_user", name, indirect=True)
@pytest.mark.parametrize("input_pwd", passwd, indirect=True)
def test_more_fixture(input_user, input_pwd):
    print("fixture返回的内容:", input_user, input_pwd)
    
    
!pytest  -v -s fixture/fixture_more_fixture.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 4 items                                                              

fixture/fixture_more_fixture.py::test_more_fixture[pwd1-name1] 登录账号为:name1
登录密码为:pwd1
fixture返回的内容: name1 pwd1
PASSED
fixture/fixture_more_fixture.py::test_more_fixture[pwd1-name2] 登录账号为:name2
登录密码为:pwd1
fixture返回的内容: name2 pwd1
PASSED
fixture/fixture_more_fixture.py::test_more_fixture[pwd2-name1] 登录账号为:name1
登录密码为:pwd2
fixture返回的内容: name1 pwd2
PASSED
fixture/fixture_more_fixture.py::test_more_fixture[pwd2-name2] 登录账号为:name2
登录密码为:pwd2
fixture返回的内容: name2 pwd2
PASSED

============================== 4 passed in 0.02s ===============================

skip、skipif跳过用例

  • pytest.mark.skip 开标记那些用例可以运行
  • 希望满足某些条件才执行某些测试用例,否则 pytest 会跳过运行该测试用例

pytest.mark.skip

跳过执行测试用例,有可选参数 reason:跳过的原因,会在执行结果中打印

  • @pytest.mark.skip 可以加在函数上、类上、方法上
  • 如果加在类上,类里面的 所有测试用例都不会执行
import pytest


@pytest.fixture(autouse=True)
def login():
    print("====登录====")


def test_case01():
    print("我是测试用例11111")


@pytest.mark.skip(reason="不执行该用例!!因为没写好!!")
def test_case02():
    print("我是测试用例22222")


class Test1:

    def test_1(self):
        print("%% 我是类测试用例1111 %%")

    @pytest.mark.skip(reason="不想执行")
    def test_2(self):
        print("%% 我是类测试用例2222 %%")


@pytest.mark.skip(reason="类也可以跳过不执行")
class TestSkip:
    def test_1(self):
        print("%% 不会执行 %%")

!pytest skip_skipif.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 5 items                                                              

skip_skipif.py .s.ss                                                     [100%]

========================= 2 passed, 3 skipped in 0.02s =========================

pytest.mark()

  • 作用:在测试用例执行期间强制跳过不再执行剩余内容
  • 类似于在python循环中,满足某些条件则 break 跳出循环
def test_function():
    n = 1
    while True:
        print(f"这是我第{n}条用例")
        n += 1
        if n == 5:
            pytest.skip("我跑五次了不跑了")
            
test_function()
这是我第1条用例
这是我第2条用例
这是我第3条用例
这是我第4条用例



---------------------------------------------------------------------------

Skipped                                   Traceback (most recent call last)

<ipython-input-76-4408d6b29e56> in <module>
      7             pytest.skip("我跑五次了不跑了")
      8 
----> 9 test_function()


<ipython-input-76-4408d6b29e56> in test_function()
      5         n += 1
      6         if n == 5:
----> 7             pytest.skip("我跑五次了不跑了")
      8 
      9 test_function()


~/.local/lib/python3.6/site-packages/_pytest/outcomes.py in skip(msg, allow_module_level)
    143     """
    144     __tracebackhide__ = True
--> 145     raise Skipped(msg=msg, allow_module_level=allow_module_level)
    146 
    147 


Skipped: 我跑五次了不跑了

pytest.skip(msg="", allow_module_level=False)

当 allow_module_level=True,可以设置在模块级别跳过整个模块

import sys
import pytest

if sys.platform.startswith("win"):
    pytest.skip("skipping windows-only tests", allow_module_level=True)


@pytest.fixture(autouse=True)
def login():
    print("====登录====")


def test_case01():
    print("我是测试用例11111")
    

!pytest skip_allow_module_level.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

skip_allow_module_level.py .                                             [100%]

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

pytest.mark.skipif(condition, reason="")

  • 作用:希望有条件地跳过某些测试用例
  • condition返回True 才会跳过
@pytest.mark.skipif(sys.platform == 'linux', reason="does not run on windows")
class TestSkipIf(object):
    def test_function(self):
        print("不能在window上运行")
        


!pytest skip_makr_skipif.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 1 item                                                               

skip_makr_skipif.py .                                                    [100%]

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

跳过标记

  • 可以将 pytest.mark.skip 和 pytest.mark.skipif 赋值给一个标记变量
  • 在不同模块之间共享这个标记变量
  • 若有多个模块的测试用例需要用到相同的 skip 或 skipif ,可以用一个单独的文件去管理这些通用标记,然后适用于整个测试用例集
# 标记
skipmark = pytest.mark.skip(reason="不能在window上运行=====")
skipifmark = pytest.mark.skipif(sys.platform == 'win32', reason="不能在window上运行啦啦啦=====")


@skipmark
class TestSkip_Mark(object):

    @skipifmark
    def test_function(self):
        print("测试标记")

    def test_def(self):
        print("测试标记")


@skipmark
def test_skip():
    print("测试标记")
    

!pytest skip_variable.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
collected 3 items                                                              

skip_variable.py sss                                                     [100%]

============================== 3 skipped in 0.01s ==============================

pytest.importtoskip(modname:str, minversion:Optional[str]=None,reason:Optional[str]=None)

  • 作用:如果缺少某些导入,则跳过模块中的所有测试

  • 参数列表

    • modname:模块名
    • minversion:版本号
    • reasone:跳过原因,默认不给也行
pexpect = pytest.importorskip("pexpect", minversion="0.3")


@pexpect
def test_import():
    print("test")
    

!pytest skip_importtoskip.py
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

<ipython-input-88-05d178e742c2> in <module>
      2 
      3 
----> 4 @pexpect
      5 def test_import():
      6     print("test")


TypeError: 'module' object is not callable

自定义标记mark

pytest 可以支持自定义标记,自定义标记可以把一个 web 项目划分多个模块,然后指定模块名称执行

mark的基本用法

  • 执行特定标记的用例
  • 执行不包含某一类标记的用例
  • 解决warning问题
import pytest


@pytest.mark.toutiao
def test_toutiao():
    print("测试头条")
    
@pytest.mark.weibo
def test_weibo():
    print("测试微博")
    
@pytest.mark.toutiao
def test_toutiao1():
    print("再次测试头条")
    
@pytest.mark.xinlang
class TestClass:
    def test_method(self):
        print("测试新浪")
        
"""没有标记测试"""
def test_nomark():
    print("没有标记测试")


!pytest -s -m weibo mark/mark.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 5 items / 4 deselected / 1 selected                                  

mark/mark.py 测试微博
.

=============================== warnings summary ===============================
mark/mark.py:4
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:4: PytestUnknownMarkWarning: Unknown pytest.mark.toutiao - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.toutiao

mark/mark.py:12
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:12: PytestUnknownMarkWarning: Unknown pytest.mark.toutiao - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.toutiao

mark/mark.py:16
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:16: PytestUnknownMarkWarning: Unknown pytest.mark.xinlang - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.xinlang

-- Docs: https://docs.pytest.org/en/latest/warnings.html
================= 1 passed, 4 deselected, 3 warnings in 0.02s ==================
"""
问题:上面的执行结果是有 warning 的
解决方案:在 同级目录 下添加 pytest.ini 配置文件,加上自定义mark
注意:pytest.ini需要和运行的测试用例同一个目录,或在根目录下作用于全局
"""

!pytest -s -m weibo mark/mark.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 5 items / 4 deselected / 1 selected                                  

mark/mark.py 测试微博
.

=============================== warnings summary ===============================
mark/mark.py:4
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:4: PytestUnknownMarkWarning: Unknown pytest.mark.toutiao - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.toutiao

mark/mark.py:12
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:12: PytestUnknownMarkWarning: Unknown pytest.mark.toutiao - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.toutiao

mark/mark.py:16
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:16: PytestUnknownMarkWarning: Unknown pytest.mark.xinlang - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.xinlang

-- Docs: https://docs.pytest.org/en/latest/warnings.html
================= 1 passed, 4 deselected, 3 warnings in 0.01s ==================
"""如果不想执行标记 weibo 的用例,取反即可"""
!pytest -s -m "no weibo" mark/mark.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 5 items                                                              
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/mark/legacy.py", line 86, in matchmark
INTERNALERROR>     return eval(markexpr, {}, MarkMapping.from_item(colitem))
INTERNALERROR>   File "<string>", line 1
INTERNALERROR>     no weibo
INTERNALERROR>            ^
INTERNALERROR> SyntaxError: unexpected EOF while parsing
INTERNALERROR> 
INTERNALERROR> During handling of the above exception, another exception occurred:
INTERNALERROR> 
INTERNALERROR> Traceback (most recent call last):
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/main.py", line 191, in wrap_session
INTERNALERROR>     session.exitstatus = doit(config, session) or 0
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/main.py", line 246, in _main
INTERNALERROR>     config.hook.pytest_collection(session=session)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/hooks.py", line 286, in __call__
INTERNALERROR>     return self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/manager.py", line 87, in <lambda>
INTERNALERROR>     firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/callers.py", line 187, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/main.py", line 257, in pytest_collection
INTERNALERROR>     return session.perform_collect()
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/main.py", line 455, in perform_collect
INTERNALERROR>     session=self, config=self.config, items=items
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/hooks.py", line 286, in __call__
INTERNALERROR>     return self._hookexec(self, self.get_hookimpls(), kwargs)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/manager.py", line 93, in _hookexec
INTERNALERROR>     return self._inner_hookexec(hook, methods, kwargs)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/manager.py", line 87, in <lambda>
INTERNALERROR>     firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/callers.py", line 208, in _multicall
INTERNALERROR>     return outcome.get_result()
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/callers.py", line 80, in get_result
INTERNALERROR>     raise ex[1].with_traceback(ex[2])
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/pluggy/callers.py", line 187, in _multicall
INTERNALERROR>     res = hook_impl.function(*args)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/mark/__init__.py", line 151, in pytest_collection_modifyitems
INTERNALERROR>     deselect_by_mark(items, config)
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/mark/__init__.py", line 139, in deselect_by_mark
INTERNALERROR>     if matchmark(item, matchexpr):
INTERNALERROR>   File "/home/ubuntu/.local/lib/python3.6/site-packages/_pytest/mark/legacy.py", line 88, in matchmark
INTERNALERROR>     raise SyntaxError(str(e) + "\nMarker expression must be valid Python!")
INTERNALERROR>   File "<string>", line None
INTERNALERROR> SyntaxError: unexpected EOF while parsing (<string>, line 1)
INTERNALERROR> Marker expression must be valid Python!

============================= 3 warnings in 0.02s ==============================
"""执行多个自定义标记的用例"""
!pytest -s -m "toutiao or weibo" mark/mark.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 5 items / 2 deselected / 3 selected                                  

mark/mark.py 测试头条
.测试微博
.再次测试头条
.

=============================== warnings summary ===============================
mark/mark.py:4
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:4: PytestUnknownMarkWarning: Unknown pytest.mark.toutiao - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.toutiao

mark/mark.py:12
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:12: PytestUnknownMarkWarning: Unknown pytest.mark.toutiao - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.toutiao

mark/mark.py:16
  /home/ubuntu/MySpace/Python/pytest/mark/mark.py:16: PytestUnknownMarkWarning: Unknown pytest.mark.xinlang - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/latest/mark.html
    @pytest.mark.xinlang

-- Docs: https://docs.pytest.org/en/latest/warnings.html
================= 3 passed, 2 deselected, 3 warnings in 0.02s ==================

参数化@pytest.mark.parametrize

  • pytest允许在多个级别启用测试参数化:

    • pytest.fixture() 允许fixture有参数化功能(后面讲解)
    • @pytest.mark.parametrize 允许在测试函数或类中定义多组参数和fixtures
    • pytest_generate_tests 允许定义自定义参数化方案或扩展(拓展)
  • 那么什么情况下可以使用参数化呢?
    ——只有测试数据和期望结果不一样,但操作步骤是一样的测试用例可以用上参数化,例如下面的例子

def add_sample(a, b):
    return a + b

def test_1():
    assert 3 + 5 == 9


def test_2():
    assert 2 + 4 == 6


def test_3():
    assert 6 * 9 == 42

实际应用场景:实际Web UI自动化中的开发场景,比如是一个登录框

  • 你肯定需要测试账号空、密码空、账号密码都为空、账号不存在、密码错误、账号密码正确等情况
  • 这些用例的区别就在于输入的测试数据和对应的交互结果
  • 所以我们可以只写一条登录测试用例,然后把多组测试数据和期望结果参数化,节省很多代码量
"""将上面重复的代码参数化"""
import pytest

@pytest.mark.parametrize("test_input, excepted", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
def test_eval(test_input, excepted):
    print(f"测试数据{test_input}, 期望结果{excepted}")
    assert eval(test_input) == excepted
    
    
!pytest mark/mark_parametrize.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 3 items                                                              

mark/mark_parametrize.py ..F                                             [100%]

=================================== FAILURES ===================================
______________________________ test_eval[6*9-42] _______________________________

test_input = '6*9', excepted = 42

    @pytest.mark.parametrize("test_input, excepted", [("3+5", 8), ("2+4", 6), ("6*9", 42)])
    def test_eval(test_input, excepted):
        print(f"测试数据{test_input}, 期望结果{excepted}")
>       assert eval(test_input) == excepted
E       AssertionError: assert 54 == 42
E        +  where 54 = eval('6*9')

mark/mark_parametrize.py:6: AssertionError
----------------------------- Captured stdout call -----------------------------
测试数据6*9, 期望结果42
=========================== short test summary info ============================
FAILED mark/mark_parametrize.py::test_eval[6*9-42] - AssertionError: assert 5...
========================= 1 failed, 2 passed in 0.11s ==========================

源码解读

def parametrize(self,argnames, argvalues, indirect=False, ids=None, scope=None): 
    pass
  • argnames
    • 源码解析:a comma-separated string denoting one or more argument names, or a list/tuple of argument strings.
    • 含义:参数名字
    • 格式:字符串"arg1,arg2,arg3"【需要用逗号分隔】
    • 备注:源码中写了可以是参数字符串的list或者tuple
@pytest.mark.parametrize(["name", "pwd"], [("yy1", "123"), ("yy2", "123")])  # 错的
@pytest.mark.parametrize(("name", "pwd"), [("yy1", "123"), ("yy2", "123")])  # 错的
@pytest.mark.parametrize("name,pwd", [("yy1", "123"), ("yy2", "123")])
  • argvalues

    • 源码解析:
      • The list of argvalues determines how often a test is invoked with different argument values.
      • If only one argname was specified argvalues is a list of values.【只有一个参数,则是值列表】
      • If N argnames were specified, argvalues must be a list of N-tuples, where each tuple-element specifies a value for its respective argname.【如果有多个参数,则用元组来存每一组值】
    • 含义:参数值列表
    • 格式:必须是列表,如:[ val1,val2,val3 ]
      • 如果只有一个参数,里面则是值的列表如:@pytest.mark.parametrize("username", ["yy", "yy2", "yy3"])
      • 如果有多个参数例,则需要用元组来存放值,一个元组对应一组参数的值,如:@pytest.mark.parametrize("name,pwd", [("yy1", "123"), ("yy2", "123"), ("yy3", "123")])
    • 备注:虽然源码说需要list包含tuple,但我试了下,tuple包含list,list包含list也是可以的........
  • ids

    • 含义:用例的ID
    • 格式:传一个字符串列表
    • 作用:可以标识每一个测试用例,自定义测试数据结果的显示,为了增加可读性
    • 强调:ids的长度需要与测试数据列表的长度一致
  • indirect

    • 作用:如果设置成True,则把传进来的参数当函数执行,而不是一个参数(下一篇博文即讲解)

装饰测试类

当装饰器 @pytest.mark.parametrize 装饰测试类时,会将数据集合传递给类的所有测试用例方法

import pytest

data_1 = [1, 2, 3]

@pytest.mark.parametrize('a, b, expect', data_1)
class TestParametrize:
    def test_parametrize_1(self, a, b, expect):
        print('\n测试函数11111 测试数据为\n{}-{}'.format(a, b))
        assert a + b == expect

    def test_parametrize_2(self, a, b, expect):
        print('\n测试函数22222 测试数据为\n{}-{}'.format(a, b))
        assert a + b == expect
        
        
!pytest -v mark/mark_class_parametrize.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 0 items / 1 error                                                    

==================================== ERRORS ====================================
__________________ ERROR collecting mark_class_parametrize.py __________________
../../../.local/lib/python3.6/site-packages/pluggy/hooks.py:286: in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
../../../.local/lib/python3.6/site-packages/pluggy/manager.py:93: in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
../../../.local/lib/python3.6/site-packages/pluggy/manager.py:87: in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
../../../.local/lib/python3.6/site-packages/_pytest/python.py:248: in pytest_pycollect_makeitem
    res = list(collector._genfunctions(name, obj))
../../../.local/lib/python3.6/site-packages/_pytest/python.py:415: in _genfunctions
    self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
../../../.local/lib/python3.6/site-packages/pluggy/hooks.py:324: in call_extra
    return self(**kwargs)
../../../.local/lib/python3.6/site-packages/pluggy/hooks.py:286: in __call__
    return self._hookexec(self, self.get_hookimpls(), kwargs)
../../../.local/lib/python3.6/site-packages/pluggy/manager.py:93: in _hookexec
    return self._inner_hookexec(hook, methods, kwargs)
../../../.local/lib/python3.6/site-packages/pluggy/manager.py:87: in <lambda>
    firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
../../../.local/lib/python3.6/site-packages/_pytest/python.py:139: in pytest_generate_tests
    metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)  # type: ignore[misc] # noqa: F821
../../../.local/lib/python3.6/site-packages/_pytest/python.py:922: in parametrize
    function_definition=self.definition,
../../../.local/lib/python3.6/site-packages/_pytest/mark/structures.py:114: in _for_parametrize
    if len(param.values) != len(argnames):
E   TypeError: object of type 'int' has no len()
=========================== short test summary info ============================
ERROR mark/mark_class_parametrize.py::TestParametrize - TypeError: object of ...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.37s ===============================

"笛卡尔积"-多个参数化装饰器

  • 重点知识
    • 一个函数或一个类可以装饰多个 @pytest.mark.parametrize
    • 这种方式,最终生成的用例数是n×m,比如上面的代码就是:参数a的数据有3个,参数b的数据有2个,所以最终的用例数有3*2=6条
    • 当参数化装饰器有很多个的时候,用例数都等于n×n×n×n×....
"""笛卡尔积,组合数据"""
import pytest

data_1 = [1, 2, 3]
data_2 = ['a', 'b']


@pytest.mark.parametrize('a', data_1)
@pytest.mark.parametrize('b', data_2)
def test_parametrize_1(a, b):
    print(f'笛卡尔积 测试数据为 : {a},{b}')
    
    
!pytest -v mark/mark_many_parametrize.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 6 items                                                              

mark/mark_many_parametrize.py::test_parametrize_1[a-1] PASSED            [ 16%]
mark/mark_many_parametrize.py::test_parametrize_1[a-2] PASSED            [ 33%]
mark/mark_many_parametrize.py::test_parametrize_1[a-3] PASSED            [ 50%]
mark/mark_many_parametrize.py::test_parametrize_1[b-1] PASSED            [ 66%]
mark/mark_many_parametrize.py::test_parametrize_1[b-2] PASSED            [ 83%]
mark/mark_many_parametrize.py::test_parametrize_1[b-3] PASSED            [100%]

============================== 6 passed in 0.03s ===============================

参数化-传入字典数据

"""字典"""
import pytest

data_1 = (
    {
        'user': 1,
        'pwd': 2
    },
    {
        'user': 3,
        'pwd': 4
    }
)


@pytest.mark.parametrize('dic', data_1)
def test_parametrize_1(dic):
    print(f'测试数据为\n{dic}')
    print(f'user:{dic["user"]},pwd{dic["pwd"]}')
    
    
!pytest -s mark/mark_dict_parametrize.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
collected 2 items                                                              

mark/mark_dict_parametrize.py 测试数据为
{'user': 1, 'pwd': 2}
user:1,pwd2
.测试数据为
{'user': 3, 'pwd': 4}
user:3,pwd4
.

============================== 2 passed in 0.01s ===============================

参数化-标记数据

"""标记参数化"""
import pytest

@pytest.mark.parametrize("test_input,expected", [
    ("3+5", 8),
    ("2+4", 6),
    pytest.param("6 * 9", 42, marks=pytest.mark.xfail),
    pytest.param("6*6", 42, marks=pytest.mark.skip)
])

def test_mark(test_input, expected):
    assert eval(test_input) == expected
    
    
!pytest -v mark/mark_mark_data.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest/mark, inifile: pytest.ini
plugins: rerunfailures-9.0, repeat-0.8.0
collected 4 items                                                              

mark/mark_mark_data.py::test_mark[3+5-8] PASSED                          [ 25%]
mark/mark_mark_data.py::test_mark[2+4-6] PASSED                          [ 50%]
mark/mark_mark_data.py::test_mark[6 * 9-42] XFAIL                        [ 75%]
mark/mark_mark_data.py::test_mark[6*6-42] SKIPPED                        [100%]

=================== 2 passed, 1 skipped, 1 xfailed in 0.04s ====================
"""增加可读性"""
import pytest

data_1 = [
    (1, 2, 3),
    (4, 5, 9)
]

ids = ["a:{} + b:{} = expect:{}".format(a, b, expect) for a, b, expect in data_1]

@pytest.mark.parametrize('a, b, expect', data_1, ids=ids)
class TestParametrize(object):

    def test_parametrize_1(self, a, b, expect):
        print('测试函数1测试数据为{}-{}'.format(a, b))
        assert a + b == expect

    def test_parametrize_2(self, a, b, expect):
        print('测试函数2数据为{}-{}'.format(a, b))
        assert a + b == expect
        

!pytest -v mark/mark_read.py

参数化-增加可读性

多少组数据,就要有多少个id,然后组成一个id的列表

作用:主要是为了更加清晰看到用例的含义

pytest.ini

pytest配置文件可以改变pytest的运行方式,它是一个固定的文件pytest.ini文件,读取配置信息,按指定的方式去运行

非test文件

pytest里面有些文件是非test文件

  • pytest.ini:pytest的主配置文件,可以改变pytest的默认行为
  • conftest.py:测试用例的一些fixture配置
  • init.py:识别该文件夹为python的package包
"""查看pytest.ini的配置选项"""
!pytest --help
usage: pytest [options] [file_or_dir] [file_or_dir] [...]

positional arguments:
  file_or_dir

general:
  -k EXPRESSION         only run tests which match the given substring
                        expression. An expression is a python evaluatable
                        expression where all names are substring-matched against
                        test names and their parent classes. Example: -k
                        'test_method or test_other' matches all test functions
                        and classes whose name contains 'test_method' or
                        'test_other', while -k 'not test_method' matches those
                        that don't contain 'test_method' in their names. -k 'not
                        test_method and not test_other' will eliminate the
                        matches. Additionally keywords are matched to classes
                        and functions containing extra names in their
                        'extra_keyword_matches' set, as well as functions which
                        have names assigned directly to them. The matching is
                        case-insensitive.
  -m MARKEXPR           only run tests matching given mark expression. example:
                        -m 'mark1 and not mark2'.
  --markers             show markers (builtin, plugin and per-project ones).
  -x, --exitfirst       exit instantly on first error or failed test.
  --maxfail=num         exit after first num failures or errors.
  --strict-markers, --strict
                        markers not registered in the `markers` section of the
                        configuration file raise errors.
  -c file               load configuration from `file` instead of trying to
                        locate one of the implicit configuration files.
  --continue-on-collection-errors
                        Force test execution even if collection errors occur.
  --rootdir=ROOTDIR     Define root directory for tests. Can be relative path:
                        'root_dir', './root_dir', 'root_dir/another_dir/';
                        absolute path: '/home/user/root_dir'; path with
                        variables: '$HOME/root_dir'.
  --fixtures, --funcargs
                        show available fixtures, sorted by plugin appearance
                        (fixtures with leading '_' are only shown with '-v')
  --fixtures-per-test   show fixtures per test
  --import-mode={prepend,append}
                        prepend/append to sys.path when importing test modules,
                        default is to prepend.
  --pdb                 start the interactive Python debugger on errors or
                        KeyboardInterrupt.
  --pdbcls=modulename:classname
                        start a custom interactive Python debugger on errors.
                        For example:
                        --pdbcls=IPython.terminal.debugger:TerminalPdb
  --trace               Immediately break when running each test.
  --capture=method      per-test capturing method: one of fd|sys|no|tee-sys.
  -s                    shortcut for --capture=no.
  --runxfail            report the results of xfail tests as if they were not
                        marked
  --lf, --last-failed   rerun only the tests that failed at the last run (or all
                        if none failed)
  --ff, --failed-first  run all tests but run the last failures first. This may
                        re-order tests and thus lead to repeated fixture
                        setup/teardown
  --nf, --new-first     run tests from new files first, then the rest of the
                        tests sorted by file mtime
  --cache-show=[CACHESHOW]
                        show cache contents, don't perform collection or tests.
                        Optional argument: glob (default: '*').
  --cache-clear         remove all cache contents at start of test run.
  --lfnf={all,none}, --last-failed-no-failures={all,none}
                        which tests to run with no previously (known) failures.
  --sw, --stepwise      exit on test failure and continue from last failing test
                        next time
  --stepwise-skip       ignore the first failing test but stop on the next
                        failing test

reporting:
  --durations=N         show N slowest setup/test durations (N=0 for all).
  -v, --verbose         increase verbosity.
  -q, --quiet           decrease verbosity.
  --verbosity=VERBOSE   set verbosity. Default is 0.
  -r chars              show extra test summary info as specified by chars:
                        (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed,
                        (p)assed, (P)assed with output, (a)ll except passed
                        (p/P), or (A)ll. (w)arnings are enabled by default (see
                        --disable-warnings), 'N' can be used to reset the list.
                        (default: 'fE').
  --disable-warnings, --disable-pytest-warnings
                        disable warnings summary
  -l, --showlocals      show locals in tracebacks (disabled by default).
  --tb=style            traceback print mode (auto/long/short/line/native/no).
  --show-capture={no,stdout,stderr,log,all}
                        Controls how captured stdout/stderr/log is shown on
                        failed tests. Default is 'all'.
  --full-trace          don't cut any tracebacks (default is to cut).
  --color=color         color terminal output (yes/no/auto).
  --pastebin=mode       send failed|all info to bpaste.net pastebin service.
  --junit-xml=path      create junit-xml style report file at given path.
  --junit-prefix=str    prepend prefix to classnames in junit-xml output
  --result-log=path     DEPRECATED path for machine-readable result log.

collection:
  --collect-only, --co  only collect tests, don't execute them.
  --pyargs              try to interpret all arguments as python packages.
  --ignore=path         ignore path during collection (multi-allowed).
  --ignore-glob=path    ignore path pattern during collection (multi-allowed).
  --deselect=nodeid_prefix
                        deselect item (via node id prefix) during collection
                        (multi-allowed).
  --confcutdir=dir      only load conftest.py's relative to specified dir.
  --noconftest          Don't load any conftest.py files.
  --keep-duplicates     Keep duplicate tests.
  --collect-in-virtualenv
                        Don't ignore tests in a local virtualenv directory
  --doctest-modules     run doctests in all .py modules
  --doctest-report={none,cdiff,ndiff,udiff,only_first_failure}
                        choose another output format for diffs on doctest
                        failure
  --doctest-glob=pat    doctests file matching pattern, default: test*.txt
  --doctest-ignore-import-errors
                        ignore doctest ImportErrors
  --doctest-continue-on-failure
                        for a given doctest, continue to run after the first
                        failure

test session debugging and configuration:
  --basetemp=dir        base temporary directory for this test run.(warning:
                        this directory is removed if it exists)
  -V, --version         display pytest version and information about plugins.
  -h, --help            show help message and configuration info
  -p name               early-load given plugin module name or entry point
                        (multi-allowed). To avoid loading of plugins, use the
                        `no:` prefix, e.g. `no:doctest`.
  --trace-config        trace considerations of conftest.py files.
  --debug               store internal tracing debug information in
                        'pytestdebug.log'.
  -o OVERRIDE_INI, --override-ini=OVERRIDE_INI
                        override ini option with "option=value" style, e.g. `-o
                        xfail_strict=True -o cache_dir=cache`.
  --assert=MODE         Control assertion debugging tools. 'plain' performs no
                        assertion debugging. 'rewrite' (the default) rewrites
                        assert statements in test modules on import to provide
                        assert expression information.
  --setup-only          only setup fixtures, do not execute tests.
  --setup-show          show setup of fixtures while executing tests.
  --setup-plan          show what fixtures and tests would be executed but don't
                        execute anything.

pytest-warnings:
  -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS
                        set which warnings to report, see -W option of python
                        itself.

logging:
  --no-print-logs       disable printing caught logs on failed tests.
  --log-level=LEVEL     level of messages to catch/display. Not set by default,
                        so it depends on the root/parent log handler's effective
                        level, where it is "WARNING" by default.
  --log-format=LOG_FORMAT
                        log format as used by the logging module.
  --log-date-format=LOG_DATE_FORMAT
                        log date format as used by the logging module.
  --log-cli-level=LOG_CLI_LEVEL
                        cli logging level.
  --log-cli-format=LOG_CLI_FORMAT
                        log format as used by the logging module.
  --log-cli-date-format=LOG_CLI_DATE_FORMAT
                        log date format as used by the logging module.
  --log-file=LOG_FILE   path to a file when logging will be written to.
  --log-file-level=LOG_FILE_LEVEL
                        log file logging level.
  --log-file-format=LOG_FILE_FORMAT
                        log format as used by the logging module.
  --log-file-date-format=LOG_FILE_DATE_FORMAT
                        log date format as used by the logging module.
  --log-auto-indent=LOG_AUTO_INDENT
                        Auto-indent multiline messages passed to the logging
                        module. Accepts true|on, false|off or an integer.

re-run failing tests to eliminate flaky failures:
  --reruns=RERUNS       number of times to re-run failed tests. defaults to 0.
  --reruns-delay=RERUNS_DELAY
                        add time (seconds) delay between reruns.

custom options:
  --count=COUNT         Number of times to repeat each test
  --repeat-scope={function,class,module,session}
                        Scope for repeating tests

[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:

  markers (linelist):   markers for test functions
  empty_parameter_set_mark (string):
                        default marker for empty parametersets
  norecursedirs (args): directory patterns to avoid for recursion
  testpaths (args):     directories to search for tests when no files or
                        directories are given in the command line.
  usefixtures (args):   list of default fixtures to be used with this project
  python_files (args):  glob-style file patterns for Python test module
                        discovery
  python_classes (args):
                        prefixes or glob names for Python test class discovery
  python_functions (args):
                        prefixes or glob names for Python test function and
                        method discovery
  disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool):
                        disable string escape non-ascii characters, might cause
                        unwanted side effects(use at your own risk)
  console_output_style (string):
                        console output: "classic", or with additional progress
                        information ("progress" (percentage) | "count").
  xfail_strict (bool):  default for the strict parameter of xfail markers when
                        not given explicitly (default: False)
  enable_assertion_pass_hook (bool):
                        Enables the pytest_assertion_pass hook.Make sure to
                        delete any previously generated pyc cache files.
  junit_suite_name (string):
                        Test suite name for JUnit report
  junit_logging (string):
                        Write captured log messages to JUnit report: one of
                        no|log|system-out|system-err|out-err|all
  junit_log_passing_tests (bool):
                        Capture log information for passing tests to JUnit
                        report:
  junit_duration_report (string):
                        Duration time to report: one of total|call
  junit_family (string):
                        Emit XML for schema: one of legacy|xunit1|xunit2
  doctest_optionflags (args):
                        option flags for doctests
  doctest_encoding (string):
                        encoding used for doctest files
  cache_dir (string):   cache directory path.
  filterwarnings (linelist):
                        Each line specifies a pattern for
                        warnings.filterwarnings. Processed after
                        -W/--pythonwarnings.
  log_print (bool):     default value for --no-print-logs
  log_level (string):   default value for --log-level
  log_format (string):  default value for --log-format
  log_date_format (string):
                        default value for --log-date-format
  log_cli (bool):       enable log display during test run (also known as "live
                        logging").
  log_cli_level (string):
                        default value for --log-cli-level
  log_cli_format (string):
                        default value for --log-cli-format
  log_cli_date_format (string):
                        default value for --log-cli-date-format
  log_file (string):    default value for --log-file
  log_file_level (string):
                        default value for --log-file-level
  log_file_format (string):
                        default value for --log-file-format
  log_file_date_format (string):
                        default value for --log-file-date-format
  log_auto_indent (string):
                        default value for --log-auto-indent
  faulthandler_timeout (string):
                        Dump the traceback of all threads if a test takes more
                        than TIMEOUT seconds to finish. Not available on
                        Windows.
  addopts (args):       extra command line options
  minversion (string):  minimally required pytest version

environment variables:
  PYTEST_ADDOPTS           extra command line options
  PYTEST_PLUGINS           comma-separated plugins to load during startup
  PYTEST_DISABLE_PLUGIN_AUTOLOAD set to disable plugin auto-loading
  PYTEST_DEBUG             set to enable debug tracing of pytest's internals


to see available markers type: pytest --markers
to see available fixtures type: pytest --fixtures
(shown according to specified file_or_dir or current dir if not specified; fixtures with leading '_' are only shown with the '-v' option

pytest.ini常用配置

marks

  • 作用:测试用例中添加了 @pytest.mark.webtest 装饰器,如果不添加marks选项的话,就会报warnings
  • 格式:list列表类型
  • 写法
[pytest]
markers =
    weibo: this is weibo page
    toutiao: toutiao
    xinlang: xinlang

xfail_strict

  • 作用:设置xfail_strict = True可以让那些标记为@pytest.mark.xfail但实际通过显示XPASS的测试用例被报告为失败
  • 格式:True 、False(默认),1、0
  • 写法
[pytest]

# mark标记说明
markers =
    weibo: this is weibo page
    toutiao: toutiao
    xinlang: xinlang

xfail_strict = True

addopts

  • 作用:addopts参数可以更改默认命令行选项,这个当我们在cmd输入一堆指令去执行用例的时候,就可以用该参数代替了,省去重复性的敲命令工作
  • 比如:想测试完生成报告,失败重跑两次,一共运行两次,通过分布式去测试,如果在cmd中写的话,命令会很长pytest -v --rerun=2 --count=2 --html=report.html --self-contained-html -n=auto
[pytest]

# mark
markers =
    weibo: this is weibo page
    toutiao: toutiao
    xinlang: xinlang

xfail_strict = True

# 命令行参数
addopts = -v --reruns=1 --count=2 --html=reports.html --self-contained-html -n=auto

加了addopts之后,我们在cmd中只需要敲pytest就可以生效了!!

log_cli

  • 作用:控制台实时输出日志

  • 格式:log_cli=True 或False(默认),或者log_cli=1 或 0

log_cli=0的运行结果
log_cli=1的运行结果

很明显,加了log_cli=1之后,可以清晰看到哪个package下的哪个module下的哪个测试用例是否passed还是failed;

所以平时测试代码是否有问题的情况下推荐加!!!但如果拿去批量跑测试用例的话不建议加,谁知道会不会影响运行性能呢?

norecursedirs

  • 作用:pytest 收集测试用例时,会递归遍历所有子目录,包括某些你明知道没必要遍历的目录,遇到这种情况,可以使用 norecursedirs 参数简化 pytest 的搜索工作【还是挺有用的!!!】
  • 默认设置: norecursedirs = .* build dist CVS _darcs {arch} *.egg
  • 正确写法:多个路径用空格隔开
[pytest]

norecursedirs = .* build dist CVS _darcs {arch} *.egg venv src resources log report util

更改测试用例收集规则

pytest默认的测试用例收集规则

  • 文件名以 test_*.py 文件和 *_test.py
  • 以 test_ 开头的函数
  • 以 Test 开头的类,不能包含 _init_ 方法
  • 以 test_ 开头的类里面的方法

我们是可以修改或者添加这个用例收集规则的;当然啦,是建议在原有的规则上添加的,如下配置

[pytest]

python_files =     test_*  *_test  test*
python_classes =   Test*   test*
python_functions = test_*  test*

pytest高频插件

失败重跑pytest-rerunfailures

环境配置

环境要求

  • Python 3.5, 最高 3.8, or PyPy3
  • pytest 5.0或更高版本

安装插件

#!pip3 install pytest-rerunfailures -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
Collecting pytest-rerunfailures
  Downloading http://pypi.doubanio.com/packages/25/91/a0d1ff828e6da1915e4972d76ea2b5f9a1b520f078b4197ef93eb8427b65/pytest_rerunfailures-9.0-py3-none-any.whl
Collecting pytest>=5.0 (from pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/c7/e2/c19c667f42f72716a7d03e8dd4d6f63f47d39feadd44cc1ee7ca3089862c/pytest-5.4.1-py3-none-any.whl (246kB)
    100% |████████████████████████████████| 256kB 23.5MB/s ta 0:00:01
[?25hCollecting setuptools>=40.0 (from pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/a0/df/635cdb901ee4a8a42ec68e480c49f85f4c59e8816effbf57d9e6ee8b3588/setuptools-46.1.3-py3-none-any.whl (582kB)
    100% |████████████████████████████████| 583kB 18.0MB/s ta 0:00:01
[?25hCollecting attrs>=17.4.0 (from pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/a2/db/4313ab3be961f7a763066401fb77f7748373b6094076ae2bda2806988af6/attrs-19.3.0-py2.py3-none-any.whl
Collecting pluggy<1.0,>=0.12 (from pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/a0/28/85c7aa31b80d150b772fbe4a229487bc6644da9ccb7e427dd8cc60cb8a62/pluggy-0.13.1-py2.py3-none-any.whl
Collecting packaging (from pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/62/0a/34641d2bf5c917c96db0ded85ae4da25b6cd922d6b794648d4e7e07c88e5/packaging-20.3-py2.py3-none-any.whl
Collecting py>=1.5.0 (from pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/99/8d/21e1767c009211a62a8e3067280bfce76e89c9f876180308515942304d2d/py-1.8.1-py2.py3-none-any.whl (83kB)
    100% |████████████████████████████████| 92kB 48.5MB/s ta 0:00:01
[?25hCollecting more-itertools>=4.0.0 (from pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/72/96/4297306cc270eef1e3461da034a3bebe7c84eff052326b130824e98fc3fb/more_itertools-8.2.0-py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 46.9MB/s ta 0:00:01
[?25hCollecting wcwidth (from pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/f6/d5/1ecdac957e3ea12c1b319fcdee8b6917ffaff8b4644d673c4d72d2f20b49/wcwidth-0.1.9-py2.py3-none-any.whl
Collecting importlib-metadata>=0.12; python_version < "3.8" (from pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/ad/e4/891bfcaf868ccabc619942f27940c77a8a4b45fd8367098955bb7e152fb1/importlib_metadata-1.6.0-py2.py3-none-any.whl
Collecting pyparsing>=2.0.2 (from packaging->pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl (67kB)
    100% |████████████████████████████████| 71kB 49.6MB/s ta 0:00:01
[?25hCollecting six (from packaging->pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata>=0.12; python_version < "3.8"->pytest>=5.0->pytest-rerunfailures)
  Downloading http://pypi.doubanio.com/packages/b2/34/bfcb43cc0ba81f527bc4f40ef41ba2ff4080e047acb0586b56b3d017ace4/zipp-3.1.0-py3-none-any.whl
Installing collected packages: attrs, zipp, importlib-metadata, pluggy, pyparsing, six, packaging, py, more-itertools, wcwidth, pytest, setuptools, pytest-rerunfailures
Successfully installed attrs-19.3.0 importlib-metadata-1.6.0 more-itertools-8.2.0 packaging-20.3 pluggy-0.13.1 py-1.8.1 pyparsing-2.4.7 pytest-5.4.1 pytest-rerunfailures-9.0 setuptools-46.1.3 six-1.14.0 wcwidth-0.1.9 zipp-3.1.0

配置参数

命令行参数:--reruns n(重新运行次数),--reruns-delay m(等待运行秒数)

装饰器参数:reruns=n(重新运行次数),reruns_delay=m(等待运行秒数)

重新运行所有失败用例

要重新运行所有测试失败,使用 --reruns 命令行选项,并指定要运行测试的最大次数:pytest --rerun 5 -s

运行失败的fixture或setup_class也将重新执行

要在两次重试之间增加延迟时间,使用 --reruns-delay 命令行选项,指定下次测试重新开始之前等待的秒数:pytest --reruns 5 --reruns-delay 10 -s

重新运行指定的测试用例

要将单个测试用例添加flaky装饰器 @pytest.mark.flaky(reruns=5) ,并在测试失败时自动重新运行,需要指定最大重新运行的次数

"""重新运行指定的测试用例"""
import pytest
import random

@pytest.mark.flasky(reruns=5, reruns_delay=2)
def test_example():
    assert random.choice([True, False, False])
    
!pytest -v -s plug/pytest_rerunfailures.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: rerunfailures-9.0
collected 0 items / 1 error                                                    

==================================== ERRORS ====================================
________________ ERROR collecting plug/pytest_rerunfailures.py _________________
import file mismatch:
imported module 'pytest_rerunfailures' has this __file__ attribute:
  /home/ubuntu/.local/lib/python3.6/site-packages/pytest_rerunfailures.py
which is not the same as the test file we want to collect:
  /home/ubuntu/MySpace/Python/pytest/plug/pytest_rerunfailures.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
=========================== short test summary info ============================
ERROR plug/pytest_rerunfailures.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.09s ===============================

注意事项

  • 如果指定了用例的重新运行次数,则在命令行添加--reruns对这些用例是不会生效的
  • 不可以和fixture装饰器一起使用: @pytest.fixture()
  • 该插件与pytest-xdist的 --looponfail 标志不兼容
  • 该插件与核心--pdb标志不兼容

重复执行pytest-repeat

  • 平常在做功能测试的时候,经常会遇到某个模块不稳定,偶然会出现一些bug,对于这种问题我们会针对此用例反复执行多次,最终复现出问题来
  • 自动化运行用例时候,也会出现偶然的bug,可以针对单个用例,或者针对某个模块的用例重复执行多次

环境配置

环境要求

  • Python 2.7、3.4+或PyPy
  • py.test 2.8或更高版本

插件安装

# !pip3 install pytest-repeat -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
Collecting pytest-repeat
  Downloading http://pypi.doubanio.com/packages/2e/de/c1d69002db74a99b3df0463e95066c03d82d9d2a53be738c140207134e0f/pytest_repeat-0.8.0-py2.py3-none-any.whl
Collecting pytest>=3.6 (from pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/c7/e2/c19c667f42f72716a7d03e8dd4d6f63f47d39feadd44cc1ee7ca3089862c/pytest-5.4.1-py3-none-any.whl (246kB)
    100% |████████████████████████████████| 256kB 3.8MB/s ta 0:00:011
[?25hCollecting more-itertools>=4.0.0 (from pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/72/96/4297306cc270eef1e3461da034a3bebe7c84eff052326b130824e98fc3fb/more_itertools-8.2.0-py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 1.8MB/s ta 0:00:011
[?25hCollecting pluggy<1.0,>=0.12 (from pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/a0/28/85c7aa31b80d150b772fbe4a229487bc6644da9ccb7e427dd8cc60cb8a62/pluggy-0.13.1-py2.py3-none-any.whl
Collecting attrs>=17.4.0 (from pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/a2/db/4313ab3be961f7a763066401fb77f7748373b6094076ae2bda2806988af6/attrs-19.3.0-py2.py3-none-any.whl
Collecting importlib-metadata>=0.12; python_version < "3.8" (from pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/ad/e4/891bfcaf868ccabc619942f27940c77a8a4b45fd8367098955bb7e152fb1/importlib_metadata-1.6.0-py2.py3-none-any.whl
Collecting py>=1.5.0 (from pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/99/8d/21e1767c009211a62a8e3067280bfce76e89c9f876180308515942304d2d/py-1.8.1-py2.py3-none-any.whl (83kB)
    100% |████████████████████████████████| 92kB 843kB/s ta 0:00:011
[?25hCollecting wcwidth (from pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/f6/d5/1ecdac957e3ea12c1b319fcdee8b6917ffaff8b4644d673c4d72d2f20b49/wcwidth-0.1.9-py2.py3-none-any.whl
Collecting packaging (from pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/62/0a/34641d2bf5c917c96db0ded85ae4da25b6cd922d6b794648d4e7e07c88e5/packaging-20.3-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata>=0.12; python_version < "3.8"->pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/b2/34/bfcb43cc0ba81f527bc4f40ef41ba2ff4080e047acb0586b56b3d017ace4/zipp-3.1.0-py3-none-any.whl
Collecting six (from packaging->pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Collecting pyparsing>=2.0.2 (from packaging->pytest>=3.6->pytest-repeat)
  Downloading http://pypi.doubanio.com/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl (67kB)
    100% |████████████████████████████████| 71kB 40.3MB/s ta 0:00:01
[?25hInstalling collected packages: more-itertools, zipp, importlib-metadata, pluggy, attrs, py, wcwidth, six, pyparsing, packaging, pytest, pytest-repeat
Successfully installed attrs-19.3.0 importlib-metadata-1.6.0 more-itertools-8.2.0 packaging-20.3 pluggy-0.13.1 py-1.8.1 pyparsing-2.4.7 pytest-5.4.1 pytest-repeat-0.8.0 six-1.14.0 wcwidth-0.1.9 zipp-3.1.0

重复测试直到失败

  • 如果需要验证偶现问题,可以一次又一次地运行相同的测试直到失败,这个插件将很有用
  • 可以将pytest的 -x 选项与pytest-repeat结合使用,以强制测试运行程序在第一次失败时停止
import pytest
import random

def test_flag():
    flag = random.choice([True, False])
    print(flag)
    assert flag
    
!pytest -s --count 5 -x plug/test_pytest_repeat.py
ERROR: /home/ubuntu/MySpace/Python/pytest/plug/pytest.ini:1: no section header defined


ptytest.mark.repeat(count)

如果要在代码中将某些测试用例标记为执行重复多次,可以使用 @pytest.mark.repeat(count)

@pytest.mark.repeat(5)
def test_repeat():
    print("测试用例执行")
    
    
!pytest plug/test_pytest_repeat.py::test_repeat
ERROR: /home/ubuntu/MySpace/Python/pytest/plug/pytest.ini:1: no section header defined


--repeat-scope

作用:可以覆盖默认的测试用例执行顺序,类似fixture的scope参数

  • function:默认,范围针对每个用例重复执行,再执行下一个用例
  • class:以class为用例集合单位,重复执行class里面的用例,再执行下一个
  • module:以模块为单位,重复执行模块里面的用例,再执行下一个
  • session:重复整个测试会话,即所有测试用例的执行一次,然后再执行第二次
!cat plug/test_pytest_repeat_scope.py
import pytest

class Test_repeat:
    def test_repeat3(self):
        print("测试用例执行333")

class Test_repeat2:
    def test_repeat3(self):
        print("测试用例执行444")
        
        
def test_repeat1():
    print("测试用例执行111")


def test_repeat2():
    print("测试用例执行222")


class Test_repeat:
    def test_repeat3(self):
        print("测试用例执行333")
!pytest -s --count=2 --repeat-scope=class plug/test_pytest_repeat_scope.py
ERROR: /home/ubuntu/MySpace/Python/pytest/plug/pytest.ini:1: no section header defined

!pytest -s --count=2 --repeat-scope=module plug/test_pytest_repeat_scope.py
ERROR: /home/ubuntu/MySpace/Python/pytest/plug/pytest.ini:1: no section header defined


注意事项

pytest-repeat不能与unittest.TestCase测试类一起使用。无论--count设置多少,这些测试始终仅运行一次,并显示警告

多重校验pytest-assume

pytest中可以用python的assert断言,也可以写多个断言,但一个失败,后面的断言将不再执行

而 pytest-assume即使中间的断言失败了,还是会执行后面的断言:

  • 即使断言失败,后面的断言还是会继续执行
  • 有助于我们分析和查看到底一共有哪些断言是失败的
  • 最后的代码也还会正常执行,比直接用assert更高效

环境配置

# !pip3 install pytest-assume -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
Collecting pytest-assume
  Downloading http://pypi.doubanio.com/packages/9a/a7/bd0f0289c2978082296936c3899c77b3e738c89fa56ecbaaafd826ec2f52/pytest_assume-2.2.1-py3-none-any.whl
Collecting pytest>=2.7 (from pytest-assume)
  Downloading http://pypi.doubanio.com/packages/c7/e2/c19c667f42f72716a7d03e8dd4d6f63f47d39feadd44cc1ee7ca3089862c/pytest-5.4.1-py3-none-any.whl (246kB)
    100% |████████████████████████████████| 256kB 56.4MB/s ta 0:00:01
[?25hCollecting more-itertools>=4.0.0 (from pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/72/96/4297306cc270eef1e3461da034a3bebe7c84eff052326b130824e98fc3fb/more_itertools-8.2.0-py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 43.4MB/s ta 0:00:01
[?25hCollecting wcwidth (from pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/f6/d5/1ecdac957e3ea12c1b319fcdee8b6917ffaff8b4644d673c4d72d2f20b49/wcwidth-0.1.9-py2.py3-none-any.whl
Collecting py>=1.5.0 (from pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/99/8d/21e1767c009211a62a8e3067280bfce76e89c9f876180308515942304d2d/py-1.8.1-py2.py3-none-any.whl (83kB)
    100% |████████████████████████████████| 92kB 48.0MB/s ta 0:00:01
[?25hCollecting importlib-metadata>=0.12; python_version < "3.8" (from pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/ad/e4/891bfcaf868ccabc619942f27940c77a8a4b45fd8367098955bb7e152fb1/importlib_metadata-1.6.0-py2.py3-none-any.whl
Collecting attrs>=17.4.0 (from pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/a2/db/4313ab3be961f7a763066401fb77f7748373b6094076ae2bda2806988af6/attrs-19.3.0-py2.py3-none-any.whl
Collecting packaging (from pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/62/0a/34641d2bf5c917c96db0ded85ae4da25b6cd922d6b794648d4e7e07c88e5/packaging-20.3-py2.py3-none-any.whl
Collecting pluggy<1.0,>=0.12 (from pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/a0/28/85c7aa31b80d150b772fbe4a229487bc6644da9ccb7e427dd8cc60cb8a62/pluggy-0.13.1-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata>=0.12; python_version < "3.8"->pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/b2/34/bfcb43cc0ba81f527bc4f40ef41ba2ff4080e047acb0586b56b3d017ace4/zipp-3.1.0-py3-none-any.whl
Collecting pyparsing>=2.0.2 (from packaging->pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl (67kB)
    100% |████████████████████████████████| 71kB 35.7MB/s ta 0:00:01
[?25hCollecting six (from packaging->pytest>=2.7->pytest-assume)
  Downloading http://pypi.doubanio.com/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Installing collected packages: more-itertools, wcwidth, py, zipp, importlib-metadata, attrs, pyparsing, six, packaging, pluggy, pytest, pytest-assume
Successfully installed attrs-19.3.0 importlib-metadata-1.6.0 more-itertools-8.2.0 packaging-20.3 pluggy-0.13.1 py-1.8.1 pyparsing-2.4.7 pytest-5.4.1 pytest-assume-2.2.1 six-1.14.0 wcwidth-0.1.9 zipp-3.1.0

assert 多重断言

def test_add1():
    assert 1 + 4 == 5
    assert 1 + 3 == 3
    assert 2 + 5 == 7
    assert 2 + 5 == 9
    print("测试完成")
    
!pytest -v -s plug/more_assert.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: rerunfailures-9.0, assume-2.2.1, repeat-0.8.0
collected 1 item                                                               

plug/more_assert.py::test_add1 FAILED

=================================== FAILURES ===================================
__________________________________ test_add1 ___________________________________

    def test_add1():
        assert 1 + 4 == 5
>       assert 1 + 3 == 3
E       assert 4 == 3
E         +4
E         -3

plug/more_assert.py:3: AssertionError
=========================== short test summary info ============================
FAILED plug/more_assert.py::test_add1 - assert 4 == 3
============================== 1 failed in 0.11s ===============================

pytest.assume多重断言

import pytest

def test_add2():
    pytest.assume(1 + 4 == 5)
    pytest.assume(1 + 3 == 3)
    pytest.assume(2 + 5 == 7)
    pytest.assume(2 + 5 == 9)
    print("测试完成")
    
!pytest -v -s plug/more_pytest_assume.py
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: rerunfailures-9.0, assume-2.2.1, repeat-0.8.0
collected 1 item                                                               

plug/more_pytest_assume.py::test_add2 测试完成
FAILED

=================================== FAILURES ===================================
__________________________________ test_add2 ___________________________________

tp = <class 'pytest_assume.plugin.FailedAssumption'>, value = None, tb = None

    def reraise(tp, value, tb=None):
        try:
            if value is None:
                value = tp()
            if value.__traceback__ is not tb:
>               raise value.with_traceback(tb)
E               pytest_assume.plugin.FailedAssumption: 
E               2 Failed Assumptions:
E               
E               plug/more_pytest_assume.py:5: AssumptionFailure
E               >>	pytest.assume(1 + 3 == 3)
E               AssertionError: assert False
E               
E               plug/more_pytest_assume.py:7: AssumptionFailure
E               >>	pytest.assume(2 + 5 == 9)
E               AssertionError: assert False

../../../.local/lib/python3.6/site-packages/six.py:702: FailedAssumption
=========================== short test summary info ============================
FAILED plug/more_pytest_assume.py::test_add2 - pytest_assume.plugin.FailedAss...
============================== 1 failed in 0.14s ===============================

分布式测试pytest-xdist

应用场景

  • 平常我们功能测试用例非常多时,比如有1千条用例,假设每个用例执行需要1分钟,如果单个测试人员执行需要1000分钟才能跑完
  • 当项目非常紧急时,会需要协调多个测试资源来把任务分成两部分,于是执行时间缩短一半,如果有10个小伙伴,那么执行时间就会变成十分之一,大大节省了测试时间
  • 为了节省项目测试时间,10个测试同时并行测试,这就是一种分布式场景
  • 同样道理,当我们自动化测试用例排常多的时候, 一条条按顺序执行会非常慢,pytest-xdist的出现就是为了让自动化测试用例可以分布式执行,从而节省自动化测试时间
  • pytest-xdist是属于进程级别的并发

分布式执行用例设计原则

  • 用例之间是独立的,用例之间没有依赖关系,用例可以完全独立运行【独立运行】
  • 用例执行没有顺序,随机顺序都能正常执行【随机执行】
  • 每个用例都能重复运行,运行结果不会影响其他用例【不影响其他用例】

环境配置

# !pip3 install pytest-xdist -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
Collecting pytest-xdist
  Downloading http://pypi.doubanio.com/packages/7c/8c/7f93c1d82f25a69a1c6e68189b9cf5ddce08dcaefdbd913d328b0234e13b/pytest_xdist-1.31.0-py2.py3-none-any.whl
Collecting pytest>=4.4.0 (from pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/c7/e2/c19c667f42f72716a7d03e8dd4d6f63f47d39feadd44cc1ee7ca3089862c/pytest-5.4.1-py3-none-any.whl (246kB)
    100% |████████████████████████████████| 256kB 25.6MB/s ta 0:00:01
[?25hCollecting execnet>=1.1 (from pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/d3/2e/c63af07fa471e0a02d05793c7a56a9f7d274a8489442a5dc4fb3b2b3c705/execnet-1.7.1-py2.py3-none-any.whl
Collecting pytest-forked (from pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/03/1e/81235e1fcfed57a4e679d34794d60c01a1e9a29ef5b9844d797716111d80/pytest_forked-1.1.3-py2.py3-none-any.whl
Collecting six (from pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Collecting more-itertools>=4.0.0 (from pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/72/96/4297306cc270eef1e3461da034a3bebe7c84eff052326b130824e98fc3fb/more_itertools-8.2.0-py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 49.0MB/s ta 0:00:01
[?25hCollecting pluggy<1.0,>=0.12 (from pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/a0/28/85c7aa31b80d150b772fbe4a229487bc6644da9ccb7e427dd8cc60cb8a62/pluggy-0.13.1-py2.py3-none-any.whl
Collecting wcwidth (from pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/f6/d5/1ecdac957e3ea12c1b319fcdee8b6917ffaff8b4644d673c4d72d2f20b49/wcwidth-0.1.9-py2.py3-none-any.whl
Collecting importlib-metadata>=0.12; python_version < "3.8" (from pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/ad/e4/891bfcaf868ccabc619942f27940c77a8a4b45fd8367098955bb7e152fb1/importlib_metadata-1.6.0-py2.py3-none-any.whl
Collecting packaging (from pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/62/0a/34641d2bf5c917c96db0ded85ae4da25b6cd922d6b794648d4e7e07c88e5/packaging-20.3-py2.py3-none-any.whl
Collecting py>=1.5.0 (from pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/99/8d/21e1767c009211a62a8e3067280bfce76e89c9f876180308515942304d2d/py-1.8.1-py2.py3-none-any.whl (83kB)
    100% |████████████████████████████████| 92kB 15.7MB/s ta 0:00:01    73% |███████████████████████▌        | 61kB 30.9MB/s eta 0:00:01
[?25hCollecting attrs>=17.4.0 (from pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/a2/db/4313ab3be961f7a763066401fb77f7748373b6094076ae2bda2806988af6/attrs-19.3.0-py2.py3-none-any.whl
Collecting apipkg>=1.4 (from execnet>=1.1->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/67/08/4815a09603fc800209431bec5b8bd2acf2f95abdfb558a44a42507fb94da/apipkg-1.5-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata>=0.12; python_version < "3.8"->pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/b2/34/bfcb43cc0ba81f527bc4f40ef41ba2ff4080e047acb0586b56b3d017ace4/zipp-3.1.0-py3-none-any.whl
Collecting pyparsing>=2.0.2 (from packaging->pytest>=4.4.0->pytest-xdist)
  Downloading http://pypi.doubanio.com/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl (67kB)
    100% |████████████████████████████████| 71kB 33.0MB/s ta 0:00:01
[?25hInstalling collected packages: more-itertools, zipp, importlib-metadata, pluggy, wcwidth, pyparsing, six, packaging, py, attrs, pytest, apipkg, execnet, pytest-forked, pytest-xdist
Successfully installed apipkg-1.5 attrs-19.3.0 execnet-1.7.1 importlib-metadata-1.6.0 more-itertools-8.2.0 packaging-20.3 pluggy-0.13.1 py-1.8.1 pyparsing-2.4.7 pytest-5.4.1 pytest-forked-1.1.3 pytest-xdist-1.31.0 six-1.14.0 wcwidth-0.1.9 zipp-3.1.0

pytest-xdist分布式测试原理

xdist的分布式类似于一主多从的结构,master机负责下发命令,控制slave机;slave机根据master机的命令执行特定测试任务
在xdist中,主是master,从是workers

大致原理

  • xdist会产生一个或多个workers,workers都通过master来控制
  • 每个worker负责执行完整的测试用例集,然后按照master的要求运行测试,而master机不执行测试任务

pytest-xdist分布式测试流程

创建worker

  • master会在总测试会话(test session)开始前产生一个或多个worker
  • master和worker之间是通过execnet和网关来通信的
  • 实际编译执行测试代码的worker可能是本地机器也可能是远程机器

收集测试项用例

  • 每个worker类似一个迷你型的pytest执行器
  • worker会执行一个完整的test collection过程【收集所有测试用例的过程】
  • 然后把测试用例的ids返回给master
  • master是不会执行任何测试用例集的

注意:所以为什么上面通过分布式测试的结果截图是没有输出用例的print内容,因为主机并不执行测试用例,pycharm相当于一个master

master 检查 workers 收集到的测试用例集

  • master接收到所有worker收集的测试用例集之后,master会进行一些完整性检查,以确保所有worker都收集到一样的测试用例集(包括顺序)
  • 如果检查通过,会将测试用例的ids列表转换成简单的索引列表,每个索引对应一个测试用例的在原来测试集中的位置
  • 这个方案可行的原因是:所有的节点都保存着相同的测试用例集
  • 并且使用这种方式可以节省带宽,因为master只需要告知workers需要执行的测试用例对应的索引,而不用告知完整的测试用例信息

测试用例分发

--dist-mode选项

  • each:master将完整的测试索引列表分发到每个worker

  • load:master将大约25%的测试用例以轮询的方式分发到各个worker,剩余的测试用例则会等待workers执行完测试用例以后再分发

注意:可以使用 pytest_xdist_make_scheduler 这个hook来实现自定义测试分发逻辑。

测试用例的执行

  • workers 重写了 pytest_runtestloop :pytest的默认实现是循环执行所有在test session这个对象里面收集到的测试用例
  • 但是在xdist里, workers实际上是等待master为其发送需要执行的测试用例
  • 当worker收到测试任务, 就顺序执行 pytest_runtest_protocol
  • 值得注意的一个细节是:workers 必须始终保持至少一个测试用例在的任务队列里, 以兼容 pytest_runtest_protocol(item, nextitem) hook的参数要求,为了将 nextitem传给hook
  • worker会在执行最后一个测试项前等待master的更多指令
  • 如果它收到了更多测试项, 那么就可以安全的执行 pytest_runtest_protocol , 因为这时nextitem参数已经可以确定
  • 如果它收到一个 "shutdown"信号, 那么就将 nextitem 参数设为 None, 然后执行 pytest_runtest_protocol

测试用例再分发(--dist-mode=load)

  • 当workers开始/结束执行时,会把测试结果返回给master,这样其他pytest hook比如: pytest_runtest_protocol 和 pytest_runtest_protocol 就可以正常执行
  • master在worker执行完一个测试后,基于测试执行时长以及每个work剩余测试用例综合决定是否向这个worker发送更多的测试用例

测试结束

  • 当master没有更多执行测试任务时,它会发送一个“shutdown”信号给所有worker
  • 当worker将剩余测试用例执行完后退出进程
  • master等待所有worker全部退出
  • 然此时仍需要处理诸如 pytest_runtest_logreport 等事件

pytest-xdist通过独特的测试模式扩展pytest

  • 测试运行并行化:如果有多个CPU或主机,则可以将它们用于组合的测试运行。 这样可以加快开发速度或使用远程计算机的特殊资源。
  • --looponfail:在子进程中重复运行测试。 每次运行之后,pytest都会等到项目中的文件更改后再运行之前失败的测试。 重复此过程,直到所有测试通过,然后再次执行完整运行。
  • 跨平台覆盖:您可以指定不同的Python解释程序或不同的平台,并在所有这些平台上并行运行测试。

pytest-xdist按照一定的顺序执行

pytest-xdist默认是无序执行的,可以通过 --dist 参数来控制顺序

--dist=loadscope

  • 将按照同一个模块module下的函数和同一个测试类class下的方法来分组,然后将每个测试组发给可以执行的worker,确保同一个组的测试用例在同一个进程中执行
  • 目前无法自定义分组,按类class分组优先于按模块module分组

--dist=loadfile

按照同一个文件名来分组,然后将每个测试组发给可以执行的worker,确保同一个组的测试用例在同一个进程中执行

如何让scope=session的fixture在test session中仅仅执行一次

pytest-xdist是让每个worker进程执行属于自己的测试用例集下的所有测试用例

这意味着在不同进程中,不同的测试用例可能会调用同一个scope范围级别较高(例如session)的fixture,该fixture则会被执行多次,这不符合scope=session的预期

解决方案:虽然pytest-xdist没有内置的支持来确保会话范围的夹具仅执行一次,但是可以通过使用锁定文件进行进程间通信来实现。

import pytest
from filelock import FileLock

@pytest.fixture(scope="session")
def login():
    print("====登录功能,返回账号,token===")
    with FileLock("session.lock"):
        name = "testyy"
        token = "npoi213bn4"
        # web ui自动化
        # 声明一个driver,再返回

        # 接口自动化
        # 发起一个登录请求,将token返回都可以这样写

    yield name, token
    print("====退出登录!!!====")
  • 下面的示例只需要执行一次login(因为它是只需要执行一次来定义配置选项,等等)
  • 当第一次请求这个fixture时,则会利用FileLock仅产生一次fixture数据
  • 当其他进程再次请求这个fixture时,则会从文件中读取数据

实例

"""测试项目架构"""
!tree plug/pytest_xdist/
plug/pytest_xdist/
├── conftest.py
├── test_1.py
├── test_job
│   ├── conftest.py
│   └── test_case1.py
├── test_toutiao
│   └── test_case2.py
├── test_weibo
│   ├── conftest.py
│   └── test_case3.py
└── untitled.txt

3 directories, 8 files
"""不使用分布式执行测试用例"""
!pytest -s plug/pytest_xdist/
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: xdist-1.31.0, rerunfailures-9.0, assume-2.2.1, forked-1.1.3, repeat-0.8.0
collected 30 items                                                             

plug/pytest_xdist/test_1.py ====登录功能,返回账号,token===
FFFFF
plug/pytest_xdist/test_job/test_case1.py EEEEEEEEEE
plug/pytest_xdist/test_toutiao/test_case2.py ==没有__init__测试用例,我进入头条了== ('testyy', 'npoi213bn4')
.==没有__init__测试用例,我进入头条了== ('testyy', 'npoi213bn4')
.==没有__init__测试用例,我进入头条了== ('testyy', 'npoi213bn4')
.==没有__init__测试用例,我进入头条了== ('testyy', 'npoi213bn4')
.==没有__init__测试用例,我进入头条了== ('testyy', 'npoi213bn4')
.
plug/pytest_xdist/test_weibo/test_case3.py &&& 用户 testyy 返回微博首页 &&&
查看微博热搜 0
.&&& 用户 testyy 返回微博首页 &&&
查看微博热搜 1
.&&& 用户 testyy 返回微博首页 &&&
查看微博热搜 2
.&&& 用户 testyy 返回微博首页 &&&
查看微博热搜 3
.&&& 用户 testyy 返回微博首页 &&&
查看微博热搜 4
.&&& 用户 testyy 返回微博首页 &&&
查看微博范冰冰 0
.&&& 用户 testyy 返回微博首页 &&&
查看微博范冰冰 1
.&&& 用户 testyy 返回微博首页 &&&
查看微博范冰冰 2
.&&& 用户 testyy 返回微博首页 &&&
查看微博范冰冰 3
.&&& 用户 testyy 返回微博首页 &&&
查看微博范冰冰 4
.====退出登录!!!====


==================================== ERRORS ====================================
______________________ ERROR at setup of test_case2_01[0] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[1] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[2] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[3] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[4] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_02[0] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[1] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[2] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[3] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[4] ______________________
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
=================================== FAILURES ===================================
_______________________________ test_get_info[0] _______________________________

login = ('testyy', 'npoi213bn4'), n = 0

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[1] _______________________________

login = ('testyy', 'npoi213bn4'), n = 1

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[2] _______________________________

login = ('testyy', 'npoi213bn4'), n = 2

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[3] _______________________________

login = ('testyy', 'npoi213bn4'), n = 3

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[4] _______________________________

login = ('testyy', 'npoi213bn4'), n = 4

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
=========================== short test summary info ============================
FAILED plug/pytest_xdist/test_1.py::test_get_info[0] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[1] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[2] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[3] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[4] - NameError: name 'sleep...
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[0]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[1]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[2]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[3]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[4]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[0]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[1]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[2]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[3]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[4]
=================== 5 failed, 15 passed, 10 errors in 15.27s ===================
"""分布式用例执行"""
!pytest -s -n auto plug/pytest_xdist/
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: xdist-1.31.0, rerunfailures-9.0, assume-2.2.1, forked-1.1.3, repeat-0.8.0
gw0 [30]m
FFFFFEEEEEEEEEE...............
==================================== ERRORS ====================================
______________________ ERROR at setup of test_case2_01[0] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[1] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[2] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[3] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[4] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_02[0] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[1] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[2] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[3] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[4] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
=================================== FAILURES ===================================
_______________________________ test_get_info[0] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 0

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[1] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 1

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[2] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 2

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[3] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 3

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[4] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 4

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
=========================== short test summary info ============================
FAILED plug/pytest_xdist/test_1.py::test_get_info[0] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[1] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[2] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[3] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[4] - NameError: name 'sleep...
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[0]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[1]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[2]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[3]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[4]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[0]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[1]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[2]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[3]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[4]
=================== 5 failed, 15 passed, 10 errors in 15.96s ===================
"""指定需要多少个CPU来跑用例"""
!pytest -s -n 2 plug/pytest_xdist/
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: xdist-1.31.0, rerunfailures-9.0, assume-2.2.1, forked-1.1.3, repeat-0.8.0
gw0 [30] / gw1 [30]1m
FFFFFEEEEEEEEEE...............
==================================== ERRORS ====================================
______________________ ERROR at setup of test_case2_01[0] ______________________
[gw1] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[1] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_01[2] ______________________
[gw1] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_02[1] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_01[3] ______________________
[gw1] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_02[2] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_01[4] ______________________
[gw1] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 6
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_01(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:6
______________________ ERROR at setup of test_case2_02[3] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[4] ______________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
______________________ ERROR at setup of test_case2_02[0] ______________________
[gw1] linux -- Python 3.6.9 /usr/bin/python3
file /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py, line 12
  @pytest.mark.parametrize("n", list(range(5)))
  def test_case2_02(open_51, n):
E       fixture 'open_51' not found
>       available fixtures: __pytest_repeat_step_number, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, login, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory, worker_id
>       use 'pytest --fixtures [testpath]' for help on them.

/home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_job/test_case1.py:12
=================================== FAILURES ===================================
_______________________________ test_get_info[0] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 0

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[1] _______________________________
[gw1] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 1

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[2] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 2

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[3] _______________________________
[gw1] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 3

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
_______________________________ test_get_info[4] _______________________________
[gw0] linux -- Python 3.6.9 /usr/bin/python3

login = ('testyy', 'npoi213bn4'), n = 4

    @pytest.mark.parametrize("n", list(range(5)))
    def test_get_info(login, n):
>       sleep(1)
E       NameError: name 'sleep' is not defined

plug/pytest_xdist/test_1.py:6: NameError
=========================== short test summary info ============================
FAILED plug/pytest_xdist/test_1.py::test_get_info[0] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[1] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[2] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[3] - NameError: name 'sleep...
FAILED plug/pytest_xdist/test_1.py::test_get_info[4] - NameError: name 'sleep...
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[0]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[1]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[2]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[1]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[3]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[2]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_01[4]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[3]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[4]
ERROR plug/pytest_xdist/test_job/test_case1.py::test_case2_02[0]
=================== 5 failed, 15 passed, 10 errors in 9.64s ====================
"""pytest-xdist和pytest-html很好的相结合"""
!pytest -s -n auto plug/pytest_xdist/ --html=plug/pytest_xdist/report.html --self-contained-html
ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...]
pytest: error: unrecognized arguments: --html=plug/pytest_xdist/report.html --self-contained-html
  inifile: None
  rootdir: /home/ubuntu/MySpace/Python/pytest


生成HTML报告pytest-html

环境配置

"""安装插件"""
# !pip3 install pytest-html -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
Collecting pytest-html
  Downloading http://pypi.doubanio.com/packages/00/a7/34f195c514d39b4453619b3eb284989e5adb09a2a68ac09ce3779f9b9478/pytest_html-2.1.1-py2.py3-none-any.whl
Collecting pytest>=5.0 (from pytest-html)
  Downloading http://pypi.doubanio.com/packages/c7/e2/c19c667f42f72716a7d03e8dd4d6f63f47d39feadd44cc1ee7ca3089862c/pytest-5.4.1-py3-none-any.whl (246kB)
    100% |████████████████████████████████| 256kB 49.7MB/s ta 0:00:01
[?25hCollecting pytest-metadata (from pytest-html)
  Downloading http://pypi.doubanio.com/packages/ce/8f/d0542e1aa0e23d902ce6acce2790736473da94453a36bdc7829f25734199/pytest_metadata-1.8.0-py2.py3-none-any.whl
Collecting py>=1.5.0 (from pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/99/8d/21e1767c009211a62a8e3067280bfce76e89c9f876180308515942304d2d/py-1.8.1-py2.py3-none-any.whl (83kB)
    100% |████████████████████████████████| 92kB 51.3MB/s ta 0:00:01
[?25hCollecting attrs>=17.4.0 (from pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/a2/db/4313ab3be961f7a763066401fb77f7748373b6094076ae2bda2806988af6/attrs-19.3.0-py2.py3-none-any.whl
Collecting importlib-metadata>=0.12; python_version < "3.8" (from pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/ad/e4/891bfcaf868ccabc619942f27940c77a8a4b45fd8367098955bb7e152fb1/importlib_metadata-1.6.0-py2.py3-none-any.whl
Collecting wcwidth (from pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/f6/d5/1ecdac957e3ea12c1b319fcdee8b6917ffaff8b4644d673c4d72d2f20b49/wcwidth-0.1.9-py2.py3-none-any.whl
Collecting pluggy<1.0,>=0.12 (from pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/a0/28/85c7aa31b80d150b772fbe4a229487bc6644da9ccb7e427dd8cc60cb8a62/pluggy-0.13.1-py2.py3-none-any.whl
Collecting more-itertools>=4.0.0 (from pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/72/96/4297306cc270eef1e3461da034a3bebe7c84eff052326b130824e98fc3fb/more_itertools-8.2.0-py3-none-any.whl (43kB)
    100% |████████████████████████████████| 51kB 40.3MB/s ta 0:00:01
[?25hCollecting packaging (from pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/62/0a/34641d2bf5c917c96db0ded85ae4da25b6cd922d6b794648d4e7e07c88e5/packaging-20.3-py2.py3-none-any.whl
Collecting zipp>=0.5 (from importlib-metadata>=0.12; python_version < "3.8"->pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/b2/34/bfcb43cc0ba81f527bc4f40ef41ba2ff4080e047acb0586b56b3d017ace4/zipp-3.1.0-py3-none-any.whl
Collecting six (from packaging->pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Collecting pyparsing>=2.0.2 (from packaging->pytest>=5.0->pytest-html)
  Downloading http://pypi.doubanio.com/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl (67kB)
    100% |████████████████████████████████| 71kB 39.9MB/s ta 0:00:01
[?25hInstalling collected packages: py, attrs, zipp, importlib-metadata, wcwidth, pluggy, more-itertools, six, pyparsing, packaging, pytest, pytest-metadata, pytest-html
Successfully installed attrs-19.3.0 importlib-metadata-1.6.0 more-itertools-8.2.0 packaging-20.3 pluggy-0.13.1 py-1.8.1 pyparsing-2.4.7 pytest-5.4.1 pytest-html-2.1.1 pytest-metadata-1.8.0 six-1.14.0 wcwidth-0.1.9 zipp-3.1.0

快速入门

注意事项:

  • 在将文件或链接添加到独立报告时,插件会发出warnings;
  • 在html测试报告中可能无法按预期显示文件或链接
"""在当前目录下创建一个report.html的测试报告"""
!pytest --html=report.html
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: xdist-1.31.0, rerunfailures-9.0, html-2.1.1, assume-2.2.1, forked-1.1.3, metadata-1.8.0, repeat-0.8.0
collected 39 items / 1 error / 38 selected                                     

==================================== ERRORS ====================================
_________________ ERROR collecting plug/pytest_xdist/test_1.py _________________
import file mismatch:
imported module 'test_1' has this __file__ attribute:
  /home/ubuntu/MySpace/Python/pytest/conftest/test_1.py
which is not the same as the test file we want to collect:
  /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_1.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
-- generated html file: file:///home/ubuntu/MySpace/Python/pytest/report.html --
=========================== short test summary info ============================
ERROR plug/pytest_xdist/test_1.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.23s ===============================
"""css是独立的,分享报告的时候样式会丢失,为了更好的分享发邮件展示报告,把css样式合并到html里"""
!pytest --html=report.html --self-contained-html
============================= test session starts ==============================
platform linux -- Python 3.6.9, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: /home/ubuntu/MySpace/Python/pytest
plugins: xdist-1.31.0, rerunfailures-9.0, html-2.1.1, assume-2.2.1, forked-1.1.3, metadata-1.8.0, repeat-0.8.0
collected 39 items / 1 error / 38 selected                                     

==================================== ERRORS ====================================
_________________ ERROR collecting plug/pytest_xdist/test_1.py _________________
import file mismatch:
imported module 'test_1' has this __file__ attribute:
  /home/ubuntu/MySpace/Python/pytest/conftest/test_1.py
which is not the same as the test file we want to collect:
  /home/ubuntu/MySpace/Python/pytest/plug/pytest_xdist/test_1.py
HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules
-- generated html file: file:///home/ubuntu/MySpace/Python/pytest/report.html --
=========================== short test summary info ============================
ERROR plug/pytest_xdist/test_1.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.17s ===============================
posted @ 2020-05-17 20:38  secoder  阅读(1317)  评论(0编辑  收藏  举报