1.安装

使用 pip 进行安装
$ pip install pytest

2. pytest模块规则

①自动智能发现用例:模块名以test开头、结尾(否则不会当做是测试用例)-----发现用例规则

②测试类,不能有__init__(self)初始化的方法-----测试用例的类不需要初始化

③方法(函数),必须以test_开头,类必须是Test

④pytest测试用例函数可以脱离类,可以直接使用函数的形式(测试用例的py文件直接写为函数)------unittest中无此用法

⑤类可以不继承unittest.TestSuite

(修改规则,78节50min),了解规则可以修改即可

 

3.pytest的优点

①灵活的筛选用例

②不需要手动加载用例(不需要导入加载器,运行器),不需要手动添加,手动运行,直接一行运行代码即可。

③pytest可以兼容unit test(unit test不能兼容pytest)

 

4.执行测试

①可以在pycharm中直接右键,以pytest方式运行,但是这个pycharm解释器的特有功能,不建议使用。推荐终端中运行

    -----如果的别人需要运行自己的自动化代码,拷贝代码到本机,可以直接在terminal中,cd到该项目的根目录下,运行

②.运行 pytest  testcase/test_xx.py  -sv   -k="abc"  -m="marker1" 表示运行testcase目录下的所有的测试用例

   ---- 加上testcase目录,比较安全,万一别的目录中的函数有test_开头,会被pytest检测到当作用例执行了

   ----  -s 可以把用例中的print信息显示出来

   ---- -v 显示详细的测试结果在terminal    (可以组合表示为-sv)

   ---- -h  查看 pytest 的所有选项

   ---- -k 表示关键字查询

   ---- -m 表示用例的标签名字,运行哪个用例

   ---- 生成测试报告  --html=report.html --self-contained-html      (其中,如果不加--self-contained-html  ,会自动生成一个目录,不需要该目录就加上该这cmd)

 

5.Assert

 pytest 中,assert 是编写测试的最基础工具

  assert  a==b

 

6.pytest.raises 捕获异常

在测试过程中,经常需要测试是否如期抛出预期的异常,以确定异常处理模块生效。在 pytest 中使用 pytest.raises() 进行异常捕获:

# test_raises.py

def test_raises():
    with pytest.raises(TypeError) as e:
        connect('localhost', '6379')
    exec_msg = e.value.args[0]
    assert exec_msg == 'port type must be int'

原理解析:

raises: 在断言一些代码块或者函数时会引发意料之中的异常或者其他失败的异常,导致程序无法运行时,使用 raises 捕获匹配到的异常,可以继续让代码正常运行

 

 expected_exception: 预期的异常(可以输入一个元组)

import pytest


class TestA:

    def test_fun(self):
        with pytest.raises(ZeroDivisionError) as e:
            assert 2/0


if __name__ == '__main__':
    pytest.main([TestA().test_fun()])

测试pass,不会报错。

 

7.筛选用例 

pytest查找测试用例的策略:

默认情况下,pytest 会递归查找当前目录下所有以 test 开始或结尾的测试用例module,并执行文件内的所有以 test 开始或结束的函数和方法。

7.1. 测试用例未做标记

直接执行测试脚本会同时执行所有测试函数

 

7.2.测试用例被标记

由于某种原因(如 test_func 的功能尚未开发完成),我们只想执行指定的测试函数。在 pytest 中有几种方式可以解决:

第一种,显式指定函数名,通过 :: 标记

        pytet -sv tests/test_xx.py:TestClass:test_read_data

 

第二种,使用模糊匹配,使用 -k 选项标识

       pytet -sv tests/test_xx.py  -k=test_read_data

 

第三种,使用 pytest.mark 在函数上进行标记

带标记的测试函数如:

# test_with_mark.py

@pytest.mark.finished
def test_func1():
    assert 1 == 1

@pytest.mark.unfinished
def test_func2():
    assert 1 != 1

测试时使用 -m 选择标记的测试函数:

pytest -m "finished"  tests/test_with_mark.py

 

 Tips:使用 mark,我们可以给每个函数打上不同的标记,测试时指定就可以允许所有被标记的函数。

运行测试时使用 -m 选项可以加上逻辑,如:

$ pytest -m "finished and commit"

$ pytest -m "finished and not merged"

 

8.跳过用例

上面提到 pytest 使用标记过滤测试函数,所以对于那些尚未开发完成的测试,最好的处理方式就是略过而不执行测试。

按正向的思路,我们只要通过标记指定要测试的就可以解决这个问题;但有时候的处境是我们能进行反向的操作才是最好的解决途径,即通过标记指定要跳过的测试

Pytest 使用特定的标记skip  完美的解决了这个问题。

8.1. pytest.mark.skip

# test_skip.py

@pytest.mark.skip(reason='out-of-date api')
def test_connect():
    pass

执行结果:

$ pytest tests/test-function/test_skip.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 1 item

tests\test-function\test_skip.py s                                       [100%]

========================== 1 skipped in 0.13 seconds ==========================

pytest用 -s 表示测试被跳过(SKIPPED

 8.2.pytest.mark.skipif  为测试函数指定被忽略的条件

@pytest.mark.skipif(conn.__version__ < '0.2.0',
                    reason='not supported until v0.2.0')
def test_api():
    pass

 

8.3. pytest.mark.skip_if_cli_equal("--name", "zhangsan", reason="zhangsan is not the VIP")

执行测试用例的command 有注册命令行参数, 比如--name,当name=zhangsan跳过测试时,可以使用上面的mark

 

9.预见的错误

如果我们事先知道测试函数会执行失败,但又不想直接跳过,而是希望显示的提示。

Pytest 使用 pytest.mark.xfail 实现预见错误功能:

 test_xfail.py

@pytest.mark.xfail(gen.__version__ < '0.2.0',
                   reason='not supported until v0.2.0')
def test_api():
    id_1 = gen.unique_id()
    id_2 = gen.unique_id()
    assert id_1 != id_2

执行结果:

$ pytest tests/test-function/test_xfail.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 1 item

tests\test-function\test_xfail.py x                                      [100%]

========================== 1 xfailed in 0.12 seconds ==========================

pytest 使用 x 表示预见的失败(XFAIL)。

如果预见的是失败,但实际运行测试却成功通过,pytest 使用 X 进行标记(XPASS

 

10.参数化(数据驱动)

当对一个测试函数进行测试时,通常会给函数传递多组参数。比如测试账号登陆,我们需要模拟各种千奇百怪的账号密码。

当然,我们可以把这些参数写在测试函数内部进行遍历。不过虽然参数众多,但仍然是一个测试,当某组参数导致断言失败,测试也就终止了。

通过异常捕获,我们可以保证程所有参数完整执行,但要分析测试结果就需要做不少额外的工作。

在 pytest 中,我们有更好的解决方法,就是参数化测试,即每组参数都独立执行一次测试。使用的工具就是 pytest.mark.parametrize(argnames, argvalues)

这里是一个密码长度的测试函数,其中参数名为 passwd,其可选列表包含三个值:

例子:

# test_parametrize.py

@pytest.mark.parametrize('passwd',
                      ['123456',
                       'abcdefdfs',
                       'as52345fasdf4'])
def test_passwd_length(passwd):
    assert len(passwd) >= 8

结果:

$ pytest tests/test-function/test_parametrize.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 3 items

tests\test-function\test_parametrize.py F..                              [100%]

 

例子2:用户密码校验

# test_parametrize.py

@pytest.mark.parametrize('user, passwd',
                         [('jack', 'abcdefgh'),
                          ('tom', 'a123456a')])
def test_passwd_md5(user, passwd):
    db = {
        'jack': 'e8dc4081b13434b45189a720b77b6818',
        'tom': '1702a132e769a623c1adb78353fc9503'
    }

    import hashlib

    assert hashlib.md5(passwd.encode()).hexdigest() == db[user]

结果:

$ pytest -v tests/test-function/test_parametrize.py::test_passwd_md5
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0 -- c:\anaconda3\python.exe
cachedir: .pytest_cache
rootdir: F:\self-repo\learning-pytest, inifile:
collected 2 items

tests/test-function/test_parametrize.py::test_passwd_md5[jack-abcdefgh] PASSED [ 50%]
tests/test-function/test_parametrize.py::test_passwd_md5[tom-a123456a] PASSED [100%]

========================== 2 passed in 0.04 seconds ===========================

 

每组测试的默认参数显示不清晰,我们可以使用 pytest.param 的 id 参数进行自定义。

例子3:

# test_parametrize.py

    @pytest.mark.parametrize('user, passwd',
                             [('jack', 'abcdefgh'), ('tom', 'a123456a')], ids = ['User<Jack>', 'User<Tom>'])

    def test_passwd_md5_id(self, user, passwd):
        db = {'jack': 'e8dc4081b13434b45189a720b77b6818', 'tom': '1702a132e769a623c1adb78353fc9503'}

        import hashlib
        assert hashlib.md5(passwd.encode()).hexdigest() == db[user]

结果:

============================= test session starts ==============================
collecting ... collected 3 items

pytest框架.py::TestA::test_fun PASSED                                    [ 33%]
pytest框架.py::TestA::test_passwd_md5_id[User<Jack>] PASSED              [ 66%]
pytest框架.py::TestA::test_passwd_md5_id[User<Tom>] PASSED               [100%]

============================== 3 passed in 0.02s ===============================

Process finished with exit code 0

 

11. 测试夹具 fixture 

Fixture是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。

我们可以利用fixture做任何事情,其中最常见的可能就是数据库的初始连接和最后关闭操作。

Pytest 使用 pytest.fixture() 来装饰测试用例,fixture的函数名,跟用例中调用的名字一致

# test_postcode.py

@pytest.fixture()
def postcode():
    return '010'


def test_postcode(postcode):
    assert postcode == '010'

fixture可以直接定义在各测试脚本中,就像上面的例子。更多时候,我们希望一个fixture可以在更大程度上复用,这就需要对固件进行集中管理。

  Pytest 使用文件 conftest.py 集中管理固件。

注解

1.在复杂的项目中,可以在不同的目录层级定义 conftest.py,其作用域为其所在的目录和子目录。

2.注意事项:不要自己显式调用 conftest.py,pytest 会自动调用,可以把 conftest 当做插件来理解。

 

11.1 预处理和后处理

很多时候需要在测试前进行预处理(如新建数据库连接),并在测试完成进行清理(关闭数据库连接)。

当有大量重复的这类操作,最佳实践是使用fixture来自动化所有预处理和后处理。

Pytest 使用 yield 关键词将fixture分为两部分,yield 之前的代码属于预处理,会在测试前执行;yield 之后的代码属于后处理,将在测试完成后执行。

以下测试模拟数据库查询,使用固件来模拟数据库的连接关闭:

# test_db.py

@pytest.fixture()
def db():
    print('Connection successful')

    yield

    print('Connection closed')


def search_user(user_id):
    d = {
        '001': 'xiaoming'
    }
    return d[user_id]


def test_search(db):
    assert search_user('001') == 'xiaoming'

$ pytest -s tests/fixture/test_db.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 1 item

tests\fixture\test_db.py Connection successful
.Connection closed


========================== 1 passed in 0.02 seconds ===========================

可以看到在测试成功的 . 标识前后有数据库的连接和关闭操作。

如果想更细的跟踪fixture执行,可以使用 --setup-show 选项
$ pytest --setup-show tests/fixture/test_db.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 1 item

tests\fixture\test_db.py
      SETUP    F db
        tests/fixture/test_db.py::test_search (fixtures used: db).
      TEARDOWN F db

========================== 1 passed in 0.03 seconds ===========================

 

11.2 作用域 

fixture的作用是为了抽离出重复的工作和方便复用,为了更精细化控制fixture(比如只想对数据库访问测试脚本使用自动连接关闭的固件),pytest 使用作用域来进行指定fixture的使用范围。

在定义fixture时,通过 scope 参数声明作用域,可选项有:

  • function: 函数级,每个测试函数都会执行一次fixture;
  • class: 类级别,每个测试类执行一次,所有方法都可以使用;
  • module: 模块级,每个模块执行一次,模块内函数和方法都可使用;
  • session: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。

默认的作用域为 function

# conftest.py

@pytest.fixture(scope='function') def func_scope(): pass @pytest.fixture(scope='module') def mod_scope(): pass @pytest.fixture(scope='session') def sess_scope(): pass @pytest.fixture(scope='class') def class_scope(): pass
# test_scope.py
def test_multi_scope(sess_scope, mod_scope, func_scope):
    pass
$ pytest --setup-show tests/fixture/test_scope.py::test_multi_scope
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 1 item

tests\fixture\test_scope.py
SETUP    S sess_scope
  SETUP    M mod_scope
      SETUP    F func_scope
        tests/fixture/test_scope.py::test_multi_scope (fixtures used: func_scope, mod_scope, sess_scope).
      TEARDOWN F func_scope
  TEARDOWN M mod_scope
TEARDOWN S sess_scope

========================== 1 passed in 0.10 seconds ===========================

对于类使用作用域,需要使用 pytest.mark.usefixtures (对函数和方法也适用):

# test_scope.py

@pytest.mark.usefixtures('class_scope')
class TestClassScope:
    def test_1(self):
        pass

    def test_2(self):
        pass

执行结果如下,可见所有测试函数都在固件作用范围内:

$ pytest --setup-show tests/fixture/test_scope.py::TestClassScope
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 2 items

tests\fixture\test_scope.py
    SETUP    C class_scope
        tests/fixture/test_scope.py::TestClassScope::()::test_1 (fixtures used: class_scope).
        tests/fixture/test_scope.py::TestClassScope::()::test_2 (fixtures used: class_scope).
    TEARDOWN C class_scope

========================== 2 passed in 0.03 seconds ===========================

 

11.3 自动执行

目前为止,所有fixture的使用都是手动指定,或者作为参数,或者使用 usefixtures

如果我们想让fixture自动执行,可以在定义时指定 autouse 参数。

下面是两个自动计时fixture,一个用于统计每个函数运行时间(function 作用域),一个用于计算测试总耗时(session 作用域):

# test_autouse.py

DATE_FORMAT = '%Y-%m-%d %H:%M:%S'


@pytest.fixture(scope='session', autouse=True)
def timer_session_scope():
    start = time.time()
    print('\nstart: {}'.format(time.strftime(DATE_FORMAT, time.localtime(start))))

    yield

    finished = time.time()
    print('finished: {}'.format(time.strftime(DATE_FORMAT, time.localtime(finished))))
    print('Total time cost: {:.3f}s'.format(finished - start))


@pytest.fixture(autouse=True)
def timer_function_scope():
    start = time.time()
    yield
    print(' Time cost: {:.3f}s'.format(time.time() - start))

注意下面的两个测试函数并都没有显式使用固件:

def test_1():
    time.sleep(1)


def test_2():
    time.sleep(2)

执行测试可看到,固件自动执行并完成计时任务:

$ pytest -s tests/fixture/test_autouse.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.6.1, py-1.5.2, pluggy-0.6.0
rootdir: F:\self-repo\learning-pytest, inifile:
collected 2 items

tests\fixture\test_autouse.py
start: 2018-06-12 10:16:27
. Time cost: 1.003s.
. Time cost: 2.003s.
finished: 2018-06-12 10:16:30
Total time cost: 3.016s.


========================== 2 passed in 3.11 seconds ===========================

 

 

 

模块级别

模块级别 的初始化、清除 分别 在整个模块的测试用例 执行前后执行,并且 只会执行1次 。

如下定义 setup_module 和 teardown_module 全局函数

def setup_module():
    print('\n *** 初始化-模块 ***')


def teardown_module():
    print('\n ***   清除-模块 ***')

class Test_错误密码:

    def test_C001001(self):
        print('\n用例C001001')
        assert 1 == 1
        
    def test_C001002(self):
        print('\n用例C001002')
        assert 2 == 2
        
    def test_C001003(self):
        print('\n用例C001003')
        assert 3 == 2


class Test_错误密码2:

    def test_C001021(self):
        print('\n用例C001021')
        assert 1 == 1
        
    def test_C001022(self):
        print('\n用例C001022')
        assert 2 == 2

 

执行命令 pytest cases -s ,运行结果如下

collected 5 items

cases\登录\test_错误登录.py
 *** 初始化-模块 ***

用例C001001
.
用例C001002
.
用例C001003
F
用例C001021
.
用例C001022
.
 ***   清除-模块 ***

可以发现,模块级别的初始化、清除 在 整个模块所有用例 执行前后 分别 执行1次

它主要是用来为该 模块 中 所有的测试用例做 公共的 初始化 和 清除

类级别

类级别 的初始化、清除 分别 在整个类的测试用例 执行前后执行,并且 只会执行1次

如下定义 setup_class 和 teardown_class 类方法

def setup_module():
    print('\n *** 初始化-模块 ***')

def teardown_module():
    print('\n ***   清除-模块 ***')

class Test_错误密码:

    @classmethod
    def setup_class(cls):
        print('\n === 初始化-类 ===')

    @classmethod
    def teardown_class(cls):
        print('\n === 清除 - 类 ===')
        
    def test_C001001(self):
        print('\n用例C001001')
        assert 1 == 1
        
    def test_C001002(self):
        print('\n用例C001002')
        assert 2 == 2
        
    def test_C001003(self):
        print('\n用例C001003')
        assert 3 == 2

class Test_错误密码2:

    def test_C001021(self):
        print('\n用例C001021')
        assert 1 == 1
        
    def test_C001022(self):
        print('\n用例C001022')
        assert 2 == 2

 

执行命令 pytest cases -s ,运行结果如下

collected 5 items

cases\登录\test_错误登录.py
 *** 初始化-模块 ***

 === 初始化-类 ===

用例C001001
.
用例C001002
.
用例C001003
F
 === 清除 - 类 ===

用例C001021
.
用例C001022
.
 ***   清除-模块 ***

可以发现,类级别的初始化、清除 在 整个模块所有用例 执行前后 分别 执行1次 。

它主要是用来为该  中的所有测试用例做 公共的 初始化 和 清除

方法级别

方法级别 的初始化、清除 分别 在类的 每个测试方法 执行前后执行,并且 每个用例分别执行1次

如下定义 setup_method 和 teardown_method 实例方法

def setup_module():
    print('\n *** 初始化-模块 ***')

def teardown_module():
    print('\n ***   清除-模块 ***')

class Test_错误密码:

    @classmethod
    def setup_class(cls):
        print('\n === 初始化-类 ===')

    @classmethod
    def teardown_class(cls):
        print('\n === 清除 - 类 ===')
        
    def setup_method(self):
        print('\n --- 初始化-方法  ---')

    def teardown_method(self):
        print('\n --- 清除  -方法 ---')
        
    def test_C001001(self):
        print('\n用例C001001')
        assert 1 == 1
        
    def test_C001002(self):
        print('\n用例C001002')
        assert 2 == 2
        
    def test_C001003(self):
        print('\n用例C001003')
        assert 3 == 2

class Test_错误密码2:

    def test_C001021(self):
        print('\n用例C001021')
        assert 1 == 1
        
    def test_C001022(self):
        print('\n用例C001022')
        assert 2 == 2

 

执行命令 pytest cases -s ,运行结果如下

collected 5 items

cases\登录\test_错误登录.py
 *** 初始化-模块 ***

 === 初始化-类 ===

 --- 初始化-方法  ---

用例C001001
.
 --- 清除  -方法 ---

 --- 初始化-方法  ---

用例C001002
.
 --- 清除  -方法 ---

 --- 初始化-方法  ---

用例C001003
F
 --- 清除  -方法 ---

 === 清除 - 类 ===

用例C001021
.
用例C001022
.
 ***   清除-模块 ***

可以发现,方法别的初始化、清除 在 整个模块所有用例 执行前后 分别 执行一次

目录级别

目标级别的 初始化清除,就是针对整个目录执行的初始化、清除。

我们在需要初始化的目录下面创建 一个名为 conftest.py 的文件,里面内容如下所示

import pytest 

@pytest.fixture(scope='package',autouse=True)
def st_emptyEnv():
    print(f'\n#### 初始化-目录甲')
    yield
    
    print(f'\n#### 清除-目录甲')

注意:这里清除环境的代码就是 yield 之后的代码。 这是一个生成器,具体的说明参见视频讲解。