pytest总结

*文档正在更新中---2021/10/21      Auth:Fanjiexiong*  添加了setup_method的和setup不同之处

pytest简介

pytest是python的一种单元测试框架,与python自带的unittest测试框架类似,但是比unittest框架使用起来更简洁,效率更高。根据pytest的官方网站介绍,它具有如下特点:

  • 非常容易上手,入门简单,文档丰富,文档中有很多实例可以参考
  • 能够支持简单的单元测试和复杂的功能测试
  • 支持参数化
  • 执行测试过程中可以将某些测试跳过(skip),或者对某些预期失败的case标记成失败
  • 支持重复执行(rerun)失败的case
  • 支持运行由nose, unittest编写的测试case
  • 可生成html报告
  • 方便的和持续集成工具jenkins集成
  • 可支持执行部分用例
  • 具有很多第三方插件,并且可以自定义扩展

安装pytest

pip3 install pytest

快速开始

1.新建一个test_sample.py文件,写以下代码

# content of test_sample.py
def func(x):
    return x +1

def test_answer():
    assert func(3)==5

2.打开test_sample.py所在的文件夹,cmd窗口输入:pytest test_sample.py(可以自己检索以test开头的文件夹)

# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:36:59] C:2
$ pytest test_sample.py
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 1 item                                                                                                                                                      

test_sample.py F                                                                                                                                                [100%]

============================================================================== FAILURES ===============================================================================
_____________________________________________________________________________ test_answer _____________________________________________________________________________

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

test_sample.py:14: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::test_answer - assert 4 == 5
========================================================================== 1 failed in 0.06s ==========================================================================


写个测试类

1.前面是写的一个test开头的测试函数,当用例用多个的时候,写函数就不太合适了。这时可以把多个测试用例,写到一个测试类里。

class TestClass:
    def test_one(self):
        x = "this"
        assert 'h' in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, 'check')

2.pytest会找到符合规则(test_.py和_test.py)所有测试,因此它发现两个test_前缀功能。 如果只想运行其中一个,可以指定传递文件名test_class.py来运行模块:
备注: -q, --quiet decrease verbosity( 显示简单结果)

# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:37:31] C:1
$ pytest test_sample.py -q
.F                                                                                                                                                              [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestClass.test_two __________________________________________________________________________

self = <test_sample.TestClass object at 0x7f4de8c27240>

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

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

多种方式执行用例

按关键字模糊匹配你要执行的测试用例

# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:43:55] 
$ pytest -k 'two' 
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected                                                                                                                         

test_sample.py F                                                                                                                                                [100%]

============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestClass.test_two __________________________________________________________________________

self = <test_sample.TestClass object at 0x7f8637ce4e80>

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

test_sample.py:17: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::TestClass::test_two - AssertionError: assert False
=================================================================== 1 failed, 1 deselected in 0.05s ===================================================================


# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:39:10] C:1
$ pytest -k 'one' 
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected                                                                                                                         

test_sample.py .                                                                                                                                                [100%]

=================================================================== 1 passed, 1 deselected in 0.01s ===================================================================


按节点运行

# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:44:16] C:1
$ pytest test_sample.py::TestClass::test_one
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 1 item                                                                                                                                                      

test_sample.py .                                                                                                                                                [100%]

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


根据标记mark运行

import pytest

class TestClass:
    @pytest.mark.one
    def test_one(self):
        x = "this"
        assert 'h' in x

    def test_two(self):
        x = "hello"
        assert hasattr(x, 'check')

运行:

# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:47:23] 
$ pytest -m one
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected                                                                                                                         

test_sample.py .                                                                                                                                                [100%]

========================================================================== warnings summary ===========================================================================
test_sample.py:11
  /home/tarzan/code/activatorweb-bug-to-autotest/test_sample.py:11: PytestUnknownMarkWarning: Unknown pytest.mark.one - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
    @pytest.mark.one

-- Docs: https://docs.pytest.org/en/stable/warnings.html
============================================================= 1 passed, 1 deselected, 1 warning in 0.02s ==============================================================


解决:You can register custom marks to avoid this warning

新建一个pytest.ini文件写入,格式不要变
[pytest]
markers =
    one
    two
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [11:05:39] 
$ pytest -m one 
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected                                                                                                                         

test_sample.py .                                                                                                                                                [100%]

=================================================================== 1 passed, 1 deselected in 0.01s ===================================================================


遇到错误停止运行

pytest -x test_class.py   遇到错误就停止
pytest --maxfail=2        最大遇到两个失败的就停止

使用python test_*.py方式运行

import pytest

class TestClass:
        def test_one(self):
            x = "this"
            assert 'h' in x

        def test_two(self):
            x = "hello"
            assert hasattr(x, 'check')

        def test_three(self):
            a = "hello"
            b = "hello world"
            assert a in b

if __name__ == "__main__":
    pytest.main(['-q', 'test_sample.py'])

运行:

# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [11:12:02] C:1
$ python3 test_sample.py 
.F.                                                                                                                                                             [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestClass.test_two __________________________________________________________________________

self = <test_sample.TestClass object at 0x7fad4f2204e0>

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

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

测试用例setup和teardown

  • 模块级(setup_module/teardown_module)开始于模块始末,全局的
  • 函数级(setup_function/teardown_function)只对函数用例生效(不在类中)
  • 类级(setup_class/teardown_class)只在类中前后运行一次(在类中)
  • 方法级(setup_method/teardown_method)开始于方法始末(在类中)
  • 类里面的(setup/teardown)运行在调用方法的前后

setup_function:每个测试用例前后执行。用来debug调试只有你失败的时候setup_function 的内容打印如果你用例通过他是不会显示的(终端运行)

import pytest

def setup_function():
    print("setup_function:每个用例开始前都会执行")

def teardown_function():
    print("teardown_function:每个用例结束后都会执行")

def test_one():
    print("正在执行----test_one")
    x = "this"
    assert 'h' in x

def test_two():
    print("正在执行----test_two")
    x = "hello"
    assert hasattr(x, 'check')

def test_three():
    print("正在执行----test_three")
    a = "hello"
    b = "hello world"
    assert a in b

if __name__ == "__main__":
    pytest.main(['-q', 'test_sample.py'])

运行:

# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [11:23:56] 
$ python3 test_sample.py -q
.F.                                                                                                                                                             [100%]
============================================================================== FAILURES ===============================================================================
______________________________________________________________________________ test_two _______________________________________________________________________________

    def test_two():
        print("正在执行----test_two")
        x = "hello"
>       assert hasattr(x, 'check')
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

test_sample.py:24: AssertionError
------------------------------------------------------------------------ Captured stdout setup ------------------------------------------------------------------------
setup_function:每个用例开始前都会执行
------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------
正在执行----test_two
---------------------------------------------------------------------- Captured stdout teardown -----------------------------------------------------------------------
teardown_function:每个用例结束后都会执行
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::test_two - AssertionError: assert False
1 failed, 2 passed in 0.04s

注意:如果是在终端的话不会有很多的详细日志,但是如果在pycharm中会显示所有的print

setup_module:每一个py文件之运行一次

不用多说/如果是在终端使用python test_.py话不会有很多的详细日志,但是如果在pycharm中会显示所有的print

在类中使用setup:

import pytest

class TestCase():

    def setup(self):
        print("setup: 每个用例开始前执行")

    def teardown(self):
        print("teardown: 每个用例结束后执行")

    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 test_one(self):
        print("正在执行----test_one")
        x = "this"
        assert 'h' in x

    def test_two(self):
        print("正在执行----test_two")
        x = "hello"
        assert hasattr(x, 'check')

    def test_three(self):
        print("正在执行----test_three")
        a = "hello"
        b = "hello world"
        assert a in b


if __name__ == "__main__":
    pytest.main(['-q', 'test_sample.py'])

pycharm运行:

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 3 items

test_sample.py::TestCase::test_one setup_class:所有用例执行之前
setup_method:  每个用例开始前执行
setup: 每个用例开始前执行
PASSED                                [ 33%]正在执行----test_one
teardown: 每个用例结束后执行
teardown_method:  每个用例结束后执行

test_sample.py::TestCase::test_two setup_method:  每个用例开始前执行
setup: 每个用例开始前执行
FAILED                                [ 66%]正在执行----test_two

test_sample.py:34 (TestCase.test_two)
self = <test_sample.TestCase object at 0x7fc4a975bb00>

    def test_two(self):
        print("正在执行----test_two")
        x = "hello"
>       assert hasattr(x, 'check')
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

test_sample.py:38: AssertionError
teardown: 每个用例结束后执行
teardown_method:  每个用例结束后执行

test_sample.py::TestCase::test_three setup_method:  每个用例开始前执行
setup: 每个用例开始前执行
PASSED                              [100%]正在执行----test_three
teardown: 每个用例结束后执行
teardown_method:  每个用例结束后执行
teardown_class:所有用例执行之前


=================================== FAILURES ===================================
______________________________ TestCase.test_two _______________________________

self = <test_sample.TestCase object at 0x7fc4a975bb00>

    def test_two(self):
        print("正在执行----test_two")
        x = "hello"
>       assert hasattr(x, 'check')
E       AssertionError: assert False
E        +  where False = hasattr('hello', 'check')

test_sample.py:38: AssertionError
---------------------------- Captured stdout setup -----------------------------
setup_method:  每个用例开始前执行
setup: 每个用例开始前执行
----------------------------- Captured stdout call -----------------------------
正在执行----test_two
--------------------------- Captured stdout teardown ---------------------------
teardown: 每个用例结束后执行
teardown_method:  每个用例结束后执行
=========================== short test summary info ============================
FAILED test_sample.py::TestCase::test_two - AssertionError: assert False
========================= 1 failed, 2 passed in 0.02s ==========================

Process finished with exit code 1

Assertion failed

Assertion failed

Assertion failed

从结果看出,运行的优先级:setup_class》setup_method》setup 》用例》teardown》teardown_method》teardown_class

setup和setup_method的不同之处

setup和setup_method不同之处,我查了一下大概是Pytest中的setup和teardown是为了支持unittest中的setUp所遗留的,我建议在Pytest中使用setup_method(self, method),这样你就可以查看当前的执行的测试用例名称了
==================

class TestCMD(object):

    def setup_class(self):
        pass

    def teardown_class(self):
        pass

    def setup_method(self, method):
        print("\n%s:%s" % (type(self).__name__, method.__name__))
        print(method.__doc__)
        pass

    def teardown_method(self, method):
        pass

    def test_cmd(self):
        """测试标题:输入正确的序列号能够正常激活"""
        assert "foo" != "bar"

函数和类混合

1.如果一个.py的文件里面既有函数用例又有类和方法用例,运行顺序又是怎样的呢?

# coding:utf-8
import pytest

def setup_module():
    print("setup_module:整个.py模块只执行一次")
    print("比如:所有用例开始前只打开一次浏览器")

def teardown_module():
    print("teardown_module:整个.py模块只执行一次")
    print("比如:所有用例结束只最后关闭浏览器")

def setup_function():
    print("setup_function:每个用例开始前都会执行")

def teardown_function():
    print("teardown_function:每个用例结束前都会执行")

def test_one():
    print("正在执行----test_one")
    x = "this"
    assert 'h' in x

def test_two():
    print("正在执行----test_two")
    x = "hello"
    assert hasattr(x, 'check')

class TestCase():

    def setup_class(self):
        print("setup_class:所有用例执行之前")

    def teardown_class(self):
        print("teardown_class:所有用例执行之前")

    def test_three(self):
        print("正在执行----test_three")
        x = "this"
        assert 'h' in x

    def test_four(self):
        print("正在执行----test_four")
        x = "hello"
        assert hasattr(x, 'check')

if __name__ == "__main__":
    pytest.main(["-s", "test_fixtclass.py"])

运行结果:

test_fixtclass.py setup_module:整个.py模块只执行一次
比如:所有用例开始前只打开一次浏览器
setup_function:每个用例开始前都会执行
正在执行----test_one
.teardown_function:每个用例结束前都会执行
setup_function:每个用例开始前都会执行
正在执行----test_two
Fteardown_function:每个用例结束前都会执行
setup_class:所有用例执行之前
正在执行----test_three
.正在执行----test_four
Fteardown_class:所有用例执行之前
teardown_module:整个.py模块只执行一次
比如:所有用例结束只最后关闭浏览器

2.从运行结果看出,setup_module/teardown_module的优先级是最大的,然后函数里面用到的setup_function/teardown_function与类里面的setup_class/teardown_class互不干涉

跳过测试用例

skip

import pytest

class TestClass:
    @pytest.mark.one
    def test_one(self):
        x = "this"
        assert 'h' in x

    @pytest.mark.skip(reason="no way of currently testing this")
    def test_two(self):
        x = "hello"
        assert 'h' not in x

if __name__ == '__main__':
    pytest.main(['test_test.py'])

运行后:

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items

test_test.py::TestClass::test_one PASSED                                 [ 50%]
test_test.py::TestClass::test_two SKIPPED (no way of currently testi...) [100%]
Skipped: no way of currently testing this

skipif

import pytest


class TestClass:
    @pytest.mark.one
    def test_one(self):
        x = "this"
        assert 'h' in x

    @pytest.mark.skipif(2>1, reason='如果2大于1了,才跳过')
    def test_two(self):
        x = "hello"
        assert 'h' not in x


if __name__ == '__main__':
    pytest.main(['test_test.py'])

把skipif当成一个变量,其他的py文件也可以调用

import pytest

myskip = pytest.mark.skipif(1>0,reason='1大于0的话,就跳过测试用例')


class TestClass:
    @pytest.mark.one
    def test_one(self):
        x = "this"
        assert 'h' in x

    @myskip
    def test_two(self):
        x = "hello"
        assert 'h' not in x


if __name__ == '__main__':
    pytest.main(['test_test.py'])

执行结果:

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items

test_test.py::TestClass::test_one PASSED                                 [ 50%]
test_test.py::TestClass::test_two SKIPPED (1大于0的话,就跳过测试用例)   [100%]
Skipped: 1大于0的话,就跳过测试用例

函数传参和fixture传参数request

前言

为了提高代码的复用性,我们在写用例的时候,会用到函数,然后不同的用例去调用这个函数。
比如登录操作,大部分的用例都会先登录,那就需要把登录单独抽出来写个函数,其它用例全部的调用这个登陆函数就行。
但是登录的账号不能写死,有时候我想用账号1去登录,执行用例1,用账号2去登录执行用例2,所以需要对函数传参。

fixture

import pytest

@pytest.fixture
def error_fixture():
    print('这个是setup中要运行的error_fixture')
    assert 0

def test_error(error_fixture):
    print('test_error')
    pass



if __name__ == "__main__":
    pytest.main(["-s", "test_test.py"])

运行:

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 1 item

test_test.py::test_error ERROR                                           [100%]这个是setup中要运行的error_fixture

test setup failed
@pytest.fixture
    def error_fixture():
        print('这个是setup中要运行的error_fixture')
>       assert 0
E       assert 0

test_test.py:6: AssertionError


==================================== ERRORS ====================================
_________________________ ERROR at setup of test_error _________________________

    @pytest.fixture
    def error_fixture():
        print('这个是setup中要运行的error_fixture')
>       assert 0
E       assert 0

test_test.py:6: AssertionError
---------------------------- Captured stdout setup -----------------------------
这个是setup中要运行的error_fixture
=========================== short test summary info ============================
ERROR test_test.py::test_error - assert 0
=============================== 1 error in 0.01s ===============================

登录函数传参

把登录单独出来,写一个函数,传2个参数user和psw,写用例的时候调用登录函数,输入几组user,psw参数化登录用例

测试用例传参需要用装饰器@pytest.mark.parametrize,里面写两个参数

  • 第一个参数是字符串,多个参数中间用逗号隔开
  • 第二个参数是list,多组数据用元祖类型
import pytest

test_login_data = [("admin", "111111"),  ("admin", "")]


def login(user, psw):
    '''普通登录函数'''
    print("登录账户:%s"%user)
    print("登录密码:%s"%psw)
    if psw:
        return True
    else:
        return False


@pytest.mark.parametrize("user, psw", test_login_data)
def test_login(user, psw):
    '''登录用例'''
    result = login(user, psw)
    assert result == True, "失败原因:密码为空"


if __name__ == "__main__":
    pytest.main(["-s", "test_test.py"])

执行结果

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items

test_test.py::test_login[admin-111111] PASSED                            [ 50%]登录账户:admin
登录密码:111111

test_test.py::test_login[admin-] FAILED                                  [100%]登录账户:admin
登录密码:

AssertionError: 失败原因:密码为空
False != True

Expected :True
Actual   :False
<Click to see difference>

user = 'admin', psw = ''

    @pytest.mark.parametrize("user, psw", test_login_data)
    def test_login(user, psw):
        '''登录用例'''
        result = login(user, psw)
>       assert result == True, "失败原因:密码为空"
E       AssertionError: 失败原因:密码为空
E       assert False == True

test_test.py:20: AssertionError


=================================== FAILURES ===================================
______________________________ test_login[admin-] ______________________________

user = 'admin', psw = ''

    @pytest.mark.parametrize("user, psw", test_login_data)
    def test_login(user, psw):
        '''登录用例'''
        result = login(user, psw)
>       assert result == True, "失败原因:密码为空"
E       AssertionError: 失败原因:密码为空
E       assert False == True

test_test.py:20: AssertionError
----------------------------- Captured stdout call -----------------------------
登录账户:admin
登录密码:
=========================== short test summary info ============================
FAILED test_test.py::test_login[admin-] - AssertionError: 失败原因:密码为空
========================= 1 failed, 1 passed in 0.02s ==========================

request参数

如果想把登录操作放到前置操作里,也就是用到@pytest.fixture装饰器,传参就用默认的request参数
user = request.param 这一步是接收传入的参数,本案例是传一个参数情况

import pytest

test_user_data = ["admin1", "admin2"]
i = 0

@pytest.fixture(scope="module")
def login(request):
    user = request.param
    global i
    i += 1
    print('这个会打印两次,用i的值记录i=:%s'%i)
    print("登录账户:%s"%user)
    return user

@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
    '''登录用例'''
    a = login
    print("测试用例中login的返回值:%s" % a)
    assert a != ""




if __name__ == "__main__":
    pytest.main(["-s", "test_test.py"])

执行后的:

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items

test_test.py::test_login[admin1] 这个会打印两次,用i的值记录i=:1
登录账户:admin1
PASSED                                  [ 50%]测试用例中login的返回值:admin1

test_test.py::test_login[admin2] 这个会打印两次,用i的值记录i=:2
登录账户:admin2
PASSED                                  [100%]测试用例中login的返回值:admin2


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

request传2个参数

如果用到@pytest.fixture,里面用2个参数情况,可以把多个参数用一个字典去存储,这样最终还是只传一个参数
不同的参数再从字典里面取对应key值就行,如: user = request.param["user"]

import pytest

test_user_data = [{"user": "admin1", "psw": "111111"},
                  {"user": "admin1", "psw": ""}]

@pytest.fixture(scope="module")
def login(request):
    user = request.param["user"]
    psw = request.param["psw"]
    print("登录账户:%s" % user)
    print("登录密码:%s" % psw)
    if psw:
        return True
    else:
        return False

# indirect=True 声明login是个函数
@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
    '''登录用例'''
    a = login
    print("测试用例中login的返回值:%s" % a)
    assert a, "失败原因:密码为空"




if __name__ == "__main__":
    pytest.main(["-s", "test_test.py"])

执行后的:

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items

test_test.py::test_login[login0] 登录账户:admin1
登录密码:111111
PASSED                                  [ 50%]测试用例中login的返回值:True

test_test.py::test_login[login1] 登录账户:admin1
登录密码:
FAILED                                  [100%]测试用例中login的返回值:False

test_test.py:20 (test_login[login1])
login = False

    @pytest.mark.parametrize("login", test_user_data, indirect=True)
    def test_login(login):
        '''登录用例'''
        a = login
        print("测试用例中login的返回值:%s" % a)
>       assert a, "失败原因:密码为空"
E       AssertionError: 失败原因:密码为空
E       assert False

test_test.py:26: AssertionError


=================================== FAILURES ===================================
______________________________ test_login[login1] ______________________________

login = False

    @pytest.mark.parametrize("login", test_user_data, indirect=True)
    def test_login(login):
        '''登录用例'''
        a = login
        print("测试用例中login的返回值:%s" % a)
>       assert a, "失败原因:密码为空"
E       AssertionError: 失败原因:密码为空
E       assert False

test_test.py:26: AssertionError
---------------------------- Captured stdout setup -----------------------------
登录账户:admin1
登录密码:
----------------------------- Captured stdout call -----------------------------
测试用例中login的返回值:False
=========================== short test summary info ============================
FAILED test_test.py::test_login[login1] - AssertionError: 失败原因:密码为空
========================= 1 failed, 1 passed in 0.04s ==========================

pytest框架之fixture前置和后置

一、conftest.py

  • 定义公共的fixture,多个测试类中都可以调用
  • pytest提供了conftest.py文件,可以将fixture定义在此文件中
  • 运行测试用例时,不需要去导入这个文件,会自动去查找conftest.py文件,然后去找到对用的fixture

代码实现:

conftest.py:

import pytest


@pytest.fixture()
def fun_():
    print('自动找到conftest文件导入fun_')
    print('这个是前置')
    a = 1
    yield a
    print('这个是后置')

test_test.py:

import pytest


class TestFun(object):
    def test_123(self, fun_):
        print('用例执行')


if __name__ == "__main__":
    pytest.main(["-s", "test_test.py"])

运行结果:

============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 1 item

test_test.py::TestFun::test_123 自动找到conftest文件导入fun_
这个是前置
PASSED                                   [100%]用例执行
这个是后置


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

二、前置和后置

fixture函数根据关键字yield作为前置和后置的分割线,并且yield也可以接收返回值,作用相当于return

import pytest

@pytest.fixture()
def init_demo():
    print("这是测试用例的前置")
    a = 1
    yield a  # 分割线(yield + 返回值)
    print("这是测试用例的后置")

三、用fixture装饰器调用fixture

在测试用例/测试类前面加上@pytest.mark.usefixtures('fixture函数名称')

ps:定义conftest.py文件,在此文件中可以定义多个fixture,pytest会自动搜索此文件

test_test.py: 测试类加上装饰器

import pytest


@pytest.mark.usefixtures('fun_')
class TestFun(object):
    def test_123(self):
        print('用例执行')


if __name__ == "__main__":
    pytest.main(["-s", "test_test.py"])


test_test.py: 测试用例加上装饰器

import pytest


class TestFun(object):
    @pytest.mark.usefixtures('fun_')
    def test_123(self):
        print('用例执行')


if __name__ == "__main__":
    pytest.main(["-s", "test_test.py"])

生成HTML报告

# 安装pytest-html插件
pip3 install pytest-html
# 执行命令生成报告
pytest --html=report.html
# 指定到对应的文件夹中
pytest --html=report/report.html   # 这里不要加 .  

如果我们想从app_main运行测试用例呢!!!

app_main.py

import pytest
import shutil
from pathlib import Path


if __name__ == '__main__':
    report_path = Path.cwd() / 'report'
    if report_path.exists():
        shutil.rmtree(report_path)
        report_path.mkdir()
    pytest.main(["--html", "report/report.html"])  # 这里就是把命令拆成列表中的元素

根据allure服务器生成网页版报告

# 安装allure-pytest
pip3 install allure-pytest --index-url https://pypi.douban.com/simple      # 请先卸载掉 pytest-allure-adaptor
# 安装allure启动一个服务来读取报告
https://github.com/allure-framework/allure2/releases  # 我这里安装的2.14 直接下载deb包然后安装
# 执行用例生成后会生成原始文件.json这个只是测试报告的原始文件,不能打开成html的报告
pytest --alluredir reults/
# 执行完成后pytest --alluredir report/,report目录会生成一个allure_raw的原始文件,这个只是测试报告的原始文件,不能打开成html的报告
# 这个时候需要启动allure服务器来读取对应的原始文件,
sudo allure serve reults/

allure serve report/

$ sudo allure serve report/
请输入密码
[sudo] tarzan 的密码:
验证成功
Generating report to temp directory...
Report successfully generated to /tmp/11808150245710901367/allure-report
Starting web server...
2021-08-19 10:58:24.553:INFO::main: Logging initialized @1691ms to org.eclipse.jetty.util.log.StdErrLog
Browse operation is not supported on your platform.You can use the link below to open the report manually.
java.lang.UnsupportedOperationException: The BROWSE action is not supported on the current platform!
        at java.desktop/java.awt.Desktop.checkActionSupport(Desktop.java:380)
        at java.desktop/java.awt.Desktop.browse(Desktop.java:524)
        at io.qameta.allure.Commands.openBrowser(Commands.java:222)
        at io.qameta.allure.Commands.open(Commands.java:152)
        at io.qameta.allure.Commands.serve(Commands.java:136)
        at io.qameta.allure.CommandLine.run(CommandLine.java:159)
        at java.base/java.util.Optional.orElseGet(Optional.java:369)
        at io.qameta.allure.CommandLine.main(CommandLine.java:88)
Server started at <http://127.0.1.1:46013/>. Press <Ctrl+C> to exit

然后访问这个链接就好了: http://127.0.1.1:46013/ 端口是随机的

如果我们想从app_main运行测试用例呢!!!

app_main.py

if __name__ == '__main__':
    report_path = Path.cwd() / 'report'
    if report_path.exists():
        shutil.rmtree(report_path)
        report_path.mkdir()
    results_path = Path.cwd() / 'results'
    if results_path.exists():
        shutil.rmtree(results_path)
        results_path.mkdir()
    pytest.main(["--html", "report/report.html", '--alluredir', 'results/'])

image-20210819110525647

生成详细的测试报告(并且是动态修改描述信息-TODO)

Allure提供了以下常用注解(未列出部分请访问官网了解),具体用法如下。

image-20220331092220250

  • @Epic
    敏捷的术语,定义史诗,往下再分Feature和Story。

  • @Feature
    敏捷的术语,定义功能模块,往下是Story。

  • @Story
    敏捷的术语,定义用户故事。

  • @Title
    定义用例名称。

  • @Description
    定义用例描述。

  • @Issue
    定义缺陷链接。可结合@Link使用,也可以结合配置文件使用。配置文件放到resource目录下,Allure 会替换{}为对应注解的值。

  • @TmsLink
    与@Issue类似用法,定义案例链接。

    @Link
    定义一个链接,在测试报告展现。

    @Severity
    定义用例的级别,主要有BLOCKER,CRITICAL,MINOR,NORMAL,TRIVIAL等几种类型,默认是NORMAL。

@allure.epic("项目名称: 授权管理")  # 定义项目
@allure.severity("blocker")  # 用例等级 	blocker 堵塞 、critical至关重要的  、normal 正常 、minor 轻微 、trivial微不足道的
@allure.issue("https://pms.uniontech.com/bug-view-115439.html")  # 禅道bug地址
@allure.testcase("http://149.335.82.12:8080/zentao/testcase-view-5-1.html")  # 禅道用例连接地址
@allure.feature("CMD-更换序列号")
def test_3():
    print('执行了服务器欧拉版特性3用例')
    logging.info('第一步:输入下载序列号')
    logging.info('第二步:序列号激活')
    logging.info('第三步:更换序列号')
    logging.info('第四步:清理测试环境')
    assert False
@allure.epic("项目名称: 授权管理")  # 定义项目
@allure.severity("blocker")  # 用例等级    blocker 堵塞 、critical至关重要的  、normal 正常 、minor 轻微 、trivial微不足道的
@allure.issue("https://pms.uniontech.com/bug-view-115439.html")  # 禅道bug地址
@allure.testcase("http://149.335.82.12:8080/zentao/testcase-view-5-1.html")  # 禅道用例连接地址
@allure.feature("CMD-更换序列号")
@pytest.mark.core
def test_1(setup):
    """
    测试场景是;
    """
    var = os.environ.get('PYTEST_CURRENT_TEST')
    print('+++++++++++++++'+var)
    var = pytest.request._pyfuncitem._obj.__doc__
    print(var)
    with allure.step('第一步:输入下载序列号'):
        logger.info('第一步成功')
    with allure.step('第二步:序列号激活'):
        logger.info('第二步成功')
    with allure.step('第三步:更换序列号'):
        logger.error('第三步失败')
    assert False

您可以为测试添加详细描述,以便根据需要向报告阅读器提供尽可能多的上下文

import allure

@allure.description_html("""
<h1>Test with some complicated html description</h1>
<table style="width:100%">
  <tr>
    <th>Firstname</th>
    <th>Lastname</th>
    <th>Age</th>
  </tr>
  <tr align="center">
    <td>William</td>
    <td>Smith</td>
    <td>50</td>
  </tr>
  <tr align="center">
    <td>Vasya</td>
    <td>Jackson</td>
    <td>94</td>
  </tr>
</table>
""")
def test_html_description():
    assert True

@allure.title使用特殊的装饰器可以使测试标题更具可读性。标题支持参数的占位符并支持动态替换。

import allure
import pytest


@allure.title("This test has a custom title")
def test_with_a_title():
    assert 2 + 2 == 4


@allure.title("This test has a custom title with unicode: Привет!")
def test_with_unicode_title():
    assert 3 + 3 == 6


@allure.title("Parameterized test title: adding {param1} with {param2}")
@pytest.mark.parametrize('param1,param2,expected', [
    (2, 2, 4),
    (1, 2, 5)
])
def test_with_parameterized_title(param1, param2, expected):
    assert param1 + param2 == expected


@allure.title("This title will be replaced in a test body")
def test_with_dynamic_title():
    assert 2 + 2 == 4
    allure.dynamic.title('After a successful test finish, the title was replaced with this line.')

失败重跑机制

当1000个用例中有一个用例因为网络波动导致了失败,那么我们需要添加重跑机制,比如等待10s后重新执行一次

开始教程:我们首先安装一个小插件

pip3 install pytest-rerunfailures -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

python文件中添加这个命令

pytest.main(["--html", "report/report.html", '--alluredir', 'results/', '--reruns', '2', '--reruns-delay', '10'])

命令行运行:

pytest test_case/test_product.py --reruns 2 --reruns-delay 10

针对单个用例使用装饰器来运行:

@pytest.mark.flaky(reruns=5, reruns_delay=2)
    def test_case_1(self):
        """
        查询软件工具产品-返回所有软件工具产品
        :return:
        """

        url = base_url + WebApi.software_list + '?' + 'id=&product_id=&status=&purpose_type=2&start_date=&end_date=&offset=0&limit=10'
        logger.info('获取的url' + url)
        headers = {'authtoken': self.token}
        res = requests.get(url, headers=headers)
        logger.info(res.text)
        for x in res.json().get('rows'):
            assert x.get('product_type_cn') == "软件工具所属类型1"

运行上次失败用例

“80%的bug集中在20%的模块,越是容易出现bug的模块,bug是越改越多“平常我们做手工测试的时候,比如用100个用例需要执行,其中10个用例失败了,
当开发修复完bug后,我们一般是重点测上次失败的用例。
那么自动化测试也一样,当用例特别多时,为了节省时间,第一次部分用例失败了,修复完之后,可以只测上次失败的用例。

--lf, --last-failed 只重新运行上次运行失败的用例(或如果没有失败的话会全部跑)
--ff, --failed-first 运行所有测试,但首先运行上次运行失败的测试(这可能会重新测试,从而导致重复的fixture setup/teardown)

代码实现:test_1.py

import pytest


def test_case_1():
    assert 1 != 1

def test_case_2():
    assert 1 == 1

if __name__ == '__main__':
    pytest.main(['-s'])

在终端运行:

pytest test_1.py

结果:

$ pytest test_1.py                                                                                                                  
======================================================= test session starts ========================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1                                        
rootdir: /home/tarzan/Desktop                                                                                                       
plugins: allure-pytest-2.8.6, html-3.1.1, metadata-1.11.0, rerunfailures-10.1
collected 2 items                                                                                                                  

test_1.py F. 

可以看出有一条用例失败了,这个时候我们继续打开终端运行:pytest --lf

# tarzan @ tarzan3w in ~/Desktop [8:59:18] 
$ pytest --lf                                                                                                                       
======================================================= test session starts ========================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1                                        
rootdir: /home/tarzan/Desktop                                                                                                       
plugins: allure-pytest-2.8.6, html-3.1.1, metadata-1.11.0, rerunfailures-10.1             
collected 1 item                                                                                                                   
run-last-failure: rerun previous 1 failure                                                                                          

test_1.py F                                                                                                                  [100%]

============================================================= FAILURES =============================================================
___________________________________________________________ test_case_1 ____________________________________________________________

    def test_case_1():
>       assert 1 != 1
E       assert 1 != 1

test_1.py:5: AssertionError
===================================================== short test summary info ======================================================
FAILED test_1.py::test_case_1 - assert 1 != 1
======================================================== 1 failed in 0.13s =========================================================
                                           

可以看出只运行了一条用例

断言

TODO

全局变量

pytest 在测试模块或 conftest.py文件中定义时以特殊方式处理一些全局变量

可以在conftest.py 文件中声明以排除测试目录或模块

collect_ignore = ["setup.py"]

可以在conftest.py 文件中声明以排除带有 Unix shell 样式通配符的测试目录或模块

collect_ignore_glob = ["*_ignore.py"]

获取当前用例名称

var = os.environ.get('PYTEST_CURRENT_TEST')

​ 可以在setup和teardown 还有当前用例中使用

  1. foo_module.py::test_foo (setup)
  2. foo_module.py::test_foo (call)
  3. foo_module.py::test_foo (teardown)

本文是根据上海悠悠学习的过程中梳理的,防止以后忘了好查询

posted @ 2022-05-23 16:10  Tarzen  阅读(264)  评论(0编辑  收藏  举报