pytest 使用

资料来源:https://www.cnblogs.com/eastonliu/category/1445341.html
https://learning-pytest.readthedocs.io/zh/latest/doc/intro/getting-started.html
https://mp.weixin.qq.com/s/2Hy4pm4CxPsQ2-oxm2T47g
https://www.cnblogs.com/huny/p/13512377.html

环境安装及入门

安装

pip3 install -U pytest

查看版本

pytest --version

快速入门

# test1.py

def test_passing():
    assert (1, 2, 3) == (1, 2, 3)

结果

$ pytest tests/test1.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\test1.py .                                                         [100%]

========================== 1 passed in 0.09 seconds ===========================

pytest 使用 . 标识测试成功(PASSED)。
pytest 使用 F 标识测试失败(FAILED)。

在用pytest编写测试用例时,需遵守以下规则:

  • 测试文件应该命名为test_.py或_test.py
  • 测试方法和函数应该被命名为test_。
  • 测试类应该被命名为Test

用例执行场景以及常用参数

1、执行目录及其子目录下的所有用例

pytest filename\

2、执行某一个py文件下的用例

pytest filename.py

3、-k 按关键字匹配

pytest  test_class.py -k "TestClass and not two"

4、按节点运行

运行文件中某个测试用例:

pytest test_sample.py::test_answer #文件名::函数名

运行文件中某个测试类中的某个用例:

pytest test_class.py::TestClass::test_one   #文件名::类名::函数名

5、-m执行标记用例

测试代码

# 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-function/test_with_mark.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 / 1 deselected

tests\test-function\test_with_mark.py .                                  [100%]

=================== 1 passed, 1 deselected in 0.10 seconds ====================

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

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

$ pytest -m "finished and commit"

$ pytest -m "finished and not merged"

6、-x 遇到错误时,停止测试

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

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

    def test_three(self):
        assert 3 == 5

7、maxfail 错误个数达到指定的数量时,停止测试

pytest --maxfail=1  test_class.py

在代码中运行pytest

默认是执行当前脚本所在目录下的所有用例。

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

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

if __name__ == '__main__':
    pytest.main()

加参数来指定运行规则,参数必须放在列表或元组中

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

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

    def test_three(self):
        assert 3 == 5

if __name__ == '__main__':
    pytest.main(['-q','--maxfail=1','test_class.py'])

skip跳过测试用例

1、skip 始终跳过该测试用例

import pytest


def test_001():
    assert 1 == 1


@pytest.mark.skip(reason="此条用例暂不执行")
def test_002():
    assert 1 == 1


def test_003():
    assert 1 == 1

也可以在测试执行或setup期间,通过调用pytest.skip(reason)函数强制跳过该用例:

def test_function():
    if not valid_config():
        pytest.skip("不支持该配置")

还可以在模块级别跳过整个模块:pytest.skip(reason,allow_module_level=True)

import sys
import pytest

if not sys.platform.startswith("win"):
    pytest.skip("跳过只支持Windows平台的用例",allow_module_level=True)

2、skipif指定条件下跳过测试用例

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

注意:skipif方法必须指定reason,否则会报错

我们还可以定义一个变量,根据变量返回为True或False来判断是否跳过用例,如下:

import pytest
import sys

minversion = pytest.mark.skipif(
    sys.version_info < (3, 9), reason="需要Python3.9版本以上"
)


@minversion
def test_002():
    assert 1 == 1

在另一个文件中还可以导入这个变量来重复引用

from api.test_case import minversion


@minversion
def test_003():
    assert 5 == 5

跳过整个文件的所有测试用例

import pytest
import sys

# 当前文件下的测试用例需要满足这个条件才能被执行
pytestmark = pytest.mark.skipif(sys.version_info < (3, 9), reason="需要Python3.9版本以上")


def test_001():
    assert 1 == 1


def test_002():
    assert 8 == 8

执行命令

pytest -x test_class.py

xfail标记预期失败的测试用例

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

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 ==========================

还可以在测试用例中用xfail强制标识测试预期失败:

 def test_function():
    if not valid_config():
        pytest.xfail("failing configuration (but should work)")

strict 参数设置为True, 如果出现xpass,测试用例的结果将视为失败

import pytest


@pytest.mark.xfail(strict=True,reason="这个用例实际返回结果与预期不一致")
def test_001():
    assert 1 == 2


@pytest.mark.xfail(strict=True,reason="这个用例实际返回结果与预期一致")
def test_002():
    assert 8 == 8


def test_003():
    assert 6 == 6

strict参数也可以配置到pytest.ini文件中

[pytest]
xfail_strict=true

2、run参数,不运行xfail标记的用例,但是报告中会将其列在“预期失败”(XFAIL)部分

import pytest


@pytest.mark.xfail(reason="这个用例实际返回结果与预期不一致")
def test_001():
    assert 1 == 2


@pytest.mark.xfail(run=False,reason="这个用例实际返回结果与预期一致")
def test_002():
    print("此条用例不执行")
    assert 8 == 8


def test_003():
    assert 6 == 6

3、–runxfail 忽略xfail

命令行执行时指定–runxfail,会导致所有的xfail标识不生效。

pytest  --runxfail

参数化

可以使用pytest.mark.parametrize装饰器来对测试用例进行参数化。

对列表中的对象进行循环,然后一一赋值,这个对象可以是列表,元组,字典。

# 单参数
# test_parametrize.py

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

# 多参数
import pytest
def add(a, b):
    return a + b

@pytest.mark.parametrize("a,b,expect", [
    [1, 1, 2],
    [2, 3, 5],
    [4, 5, 7]
])
def test_add(a, b, expect):
    assert add(a, b) == expect

用id进行标识

import pytest
def add(a, b):
    return a + b
@pytest.mark.parametrize("a,b,expect", [
    [1, 1, 2],
    [2, 3, 5],
    [4, 5, 9]
],ids=["第一个测试用例","第二个测试用例","第三个测试用例"])
def test_add(a, b, expect):
    assert add(a, b) == expect

或者

# test_parametrize.py

@pytest.mark.parametrize('user, passwd',
                         [pytest.param('jack', 'abcdefgh', id='User<Jack>'),
                          pytest.param('tom', 'a123456a', id='User<Tom>')])
def test_passwd_md5_id(user, passwd):
    db = {
        'jack': 'e8dc4081b13434b45189a720b77b6818',
        'tom': '1702a132e769a623c1adb78353fc9503'
    }

    import hashlib

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

解决标记中文显示unicode编码

标记时,中文在控制台显示unicode编码

在项目目录下新建一个conftest.py文件,添加如下函数:

def pytest_collection_modifyitems(items):
    for item in items:
        item.name = item.name.encode('utf-8').decode('unicode-escape')
        item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')

再次执行就可以显示中文了

使用mark.xfail对参数进行标识

import pytest
def add(a, b):
    return a + b
@pytest.mark.parametrize('a,b,expect', [
    pytest.param(1, 1, 2, id="first case"),
    pytest.param(2, 3, 5, id="second case"),
    pytest.param(4, 5, 8, id="third case", marks=pytest.mark.xfail(reason="这是个bug"))
])
def test_add(a, b, expect):
    assert add(a, b) == expect

读取外部数据进行参数化

在自动化测试用例编写中,我们一般遵循数据分离的原则,在使用pytest.mark.parametrize装饰器进行参数化时,数据来源也可以是json,yaml,csv等文件。

如下:

我们把数据放在json文件中

# param.json
{
  "par": [
    {"a": 1,"b": 2,"expect": 3},
    {"a": 2,"b": 4,"expect": 6},
    {"a": 4,"b": 5,"expect": 9}
  ]
}

测试用例:

import pytest
import json


def read_json():
    with open("param.json", "r") as f:
        return json.load(f)["par"]


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


@pytest.mark.parametrize('data', read_json())
def test_add(data):
    a = data.get("a")
    b = data.get("b")
    expect = data.get("expect")
    assert add(a, b) == expect

pytest.ini配置文件

pytest.ini配置文件可以改变pytest一些默认的运行方式,如:用例收集规则,标签,命令行参数等等。

基本格式如下:

# 新建pytest.ini文件,一般放在项目的顶级目录下,不能随意命名

[pytest]

addopts = -v --rerun=2 --count=2
xfail_strict = true

使用pytest -h参看帮助文档,找到这行:ini-options in the first pytest.ini|tox.ini|setup.cfg file found

以下是一些常用的配置:

1、addopts

更改默认命令行选项,当我们用命令行运行时,需要输入多个参数,很不方便。比如想测试完生成报告,失败重跑两次,一共运行两次,通过分布式去测试,如果在命令行中执行的话,命令会很长,很不方便

pytest -v --rerun=2 --count=2 --html=report.html --self-contained-html -n=auto

这时可以在pytest.ini文件通过addopts 加上子而写参数

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

在命令行中只需要输入pytest就可以默认以这些参数去执行了。

2、更改测试用例收集规则

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

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

如果需要修改这些规则,可以在pytest.ini文件中加入以下配置:

#测试用例收集规则
python_files =  test_*.py  *_test.py    # 文件名
python_classes = Test*                      # 类
python_functions = test_*                  # 函数

多个匹配规则以空格分开

3、指定搜索测试用例目录

testpaths = apicase     #指定用例搜索目录

只收集apicase目录下的测试用例

4、排除搜索目录

norecursedirs = tmp* *plugins

不搜索tmp的前缀文件和plugins的后缀文件

5、指定mark标签

markers =
    smoke: this is smoke case
    login: this is login case

6、xfail_strict
设置xfail_strict = True可以让那些标记为@pytest.mark.xfail但实际通过显示XPASS的测试用例被报告为失败

xfail_strict = True

经典的setup和teardown

在pytest中也有类似于unittest中的setup和teardown功能的默认函数或方法:

  • 函数级(setup_function/teardown_function)
  • 方法级(setup_method/teardown_method)
  • 类级(setup_class/teardown_class)
  • 模块级(setup_module/teardown_module)

一、函数级(setup_function/teardown_function)

用于函数形式的测试用例,每个测试用例执行前调用一次setup_function,执行结束后调用一次teardown_function,如下:

def setup_function():
    print("准备条件")


def teardown_function():
    print("销毁条件")


def test_001():
    print("执行第一个测试用例")


def test_002():
    print("执行第二个测试用例")

二、方法级(setup_method/teardown_method)

用于类中方法形式的测试用例,每个测试用例执行前调用一次setup_method,执行结束后调用一次teardown_method,如下:

class TestOne:
    def setup_method(self):
        print("准备条件")

    def teardown_method(self):
        print("销毁条件")

    def test_001(self):
        print("执行第一个测试用例")

    def test_002(self):
        print("执行第二个测试用例")

三、类级(setup_class/teardown_class)

用于类中,在一个类中只调用一次,用例开始前调用一次setup_class,用例结束后调用一次teardown_class,如下:

class TestOne:
    def setup_class(self):
        print("准备条件")

    def teardown_class(self):
        print("销毁条件")

    def test_001(self):
        print("执行第一个测试用例")

    def test_002(self):
        print("执行第二个测试用例")

四、模块级(setup_module/teardown_module)

用于当前模块,当前模块执行前调用一次setup_module,当前模块执行结束后调用一次teardown_module,如下:

def setup_module():
    print("开始执行当前模块")


def teardown_module():
    print("结束执行当前模块")


class TestOne:
    def setup_class(self):
        print("准备条件")

    def teardown_class(self):
        print("销毁条件")

    def test_001(self):
        print("执行第一个测试用例")

    def test_002(self):
        print("执行第二个测试用例")

固件

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

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

Pytest 使用 pytest.fixture() 定义固件,下面是最简单的固件,只返回北京邮编:

# test_postcode.py

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


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

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

在复杂的项目中,可以在不同的目录层级定义 conftest.py,其作用域为其所在的目录和子目录。
不要自己显式调用 conftest.py,pytest 会自动调用,可以把 conftest 当做插件来理解。

预处理和后处理

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

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

Pytest 使用 yield 关键词将固件分为两部分,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'

执行时使用 -s 阻止消息被吞:

$ 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 ===========================

可以看到在测试成功的 . 标识前后有数据库的连接和关闭操作。
如果想更细的跟踪固件执行,可以使用 --setup-show 选项

作用域

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

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

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

默认的作用域为 function。

@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 ===========================

自动执行

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

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

下面是两个自动计时固件,一个用于统计每个函数运行时间(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 ===========================

固件参数化

因为固件也是函数,我们同样可以对固件进行参数化。在什么情况下需要对固件参数化?

假设现在有一批 API 需要测试对不同数据库的支持情况(对所有数据库进行相同操作),最简单的方法就是针对每个数据库编写一个测试用例,但这包含大量重复代码,如数据库的连接、关闭,查询等。

进一步,可以使用固件抽离出数据库的通用操作,每个 API 都能复用这些数据库固件,同时可维护性也得到提升。

更进一步,可以继续将这些固件合并为一个,而通过参数控制连接到不同的数据库。这就需要使用固件参数化来实现。固件参数化需要使用 pytest 内置的固件 request,并通过 request.param 获取参数。

@pytest.fixture(params=[
    ('redis', '6379'),
    ('elasticsearch', '9200')
])
def param(request):
    return request.param


@pytest.fixture(autouse=True)
def db(param):
    print('\nSucceed to connect %s:%s' % param)

    yield

    print('\nSucceed to close %s:%s' % param)


def test_api():
    assert 1 == 1

执行结果:

$ pytest -s tests/fixture/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 2 items

tests\fixture\test_parametrize.py
Succeed to connect redis:6379
.
Succeed to close redis:6379

Succeed to connect elasticsearch:9200
.
Succeed to close elasticsearch:9200

========================== 2 passed in 0.10 seconds ===========================

与函数参数化使用 @pytest.mark.parametrize 不同,固件在定义时使用 params 参数进行参数化。

固件参数化依赖于内置固件 request 及其属性 param。

内置固件

tmpdir & tmpdir_factory

用于临时文件和目录管理,默认会在测试结束时删除。
tmpdir 只有 function 作用域,只能在函数内使用。
使用 tmpdir.mkdir() 创建目临时录,tmpdir.join() 创建临时文件(或者使用创建的目录)。

def test_tmpdir(tmpdir):
    a_dir = tmpdir.mkdir('mytmpdir')
    a_file = a_dir.join('tmpfile.txt')

    a_file.write('hello, pytest!')

    assert a_file.read() == 'hello, pytest!'

tmpdir_factory 可以在所有作用域使用,包括 function, class, module, session。

@pytest.fixture(scope='module')
def my_tmpdir_factory(tmpdir_factory):
    a_dir = tmpdir_factory.mktemp('mytmpdir')
    a_file = a_dir.join('tmpfile.txt')

    a_file.write('hello, pytest!')

    return a_file
pytestconfig

使用 pytestconfig,可以很方便的读取命令行参数和配置文件。

下面示例演示命令行参数解析:首先在 conftest.py 中使用函数 pytest_addoption (特定的 hook function ):

# conftest.py

def pytest_addoption(parser):
    parser.addoption('--host', action='store',
                     help='host of db')
    parser.addoption('--port', action='store', default='8888',
                     help='port of db')

然后就可以在测试函数中通过 pytestconfig 获取命令行参数:

# test_config.py

def test_option1(pytestconfig):
    print('host: %s' % pytestconfig.getoption('host'))
    print('port: %s' % pytestconfig.getoption('port'))

pytestconfig 其实是 request.config 的快捷方式,所以也可以自定义固件实现命令行参数读取。

# conftest.py

def pytest_addoption(parser):
    parser.addoption('--host', action='store',
                     help='host of db')
    parser.addoption('--port', action='store', default='8888',
                     help='port of db')


@pytest.fixture
def config(request):
    return request.config


# test_config.py

def test_option2(config):
    print('host: %s' % config.getoption('host'))
    print('port: %s' % config.getoption('port'))

执行结果:

$ pytest -s --host=localhost tests/fixture/test_config.py::test_option2
============================= 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_config.py host: localhost
port: 8888
.

========================== 1 passed in 0.06 seconds ===========================
capsys

capsys 用于捕获 stdout 和 stderr 的内容,并临时关闭系统输出。

# test_capsys.py

def ping(output):
    print('Pong...', file=output)


def test_stdout(capsys):
    ping(sys.stdout)
    out, err = capsys.readouterr()
    assert out == 'Pong...\n'
    assert err == ''


def test_stderr(capsys):
    ping(sys.stderr)
    out, err = capsys.readouterr()
    assert out == ''
    assert err == 'Pong...\n'
monkeypatch

monkeypath 用于运行时动态修改类或模块。
一个简单的 monkeypatch 如:

from SomeOtherProduct.SomeModule import SomeClass

def speak(self):
    return "ook ook eee eee eee!"

SomeClass.speak = speak

Pytest 内置 monkeypatch 提供的函数有:

  • setattr(target, name, value, raising=True),设置属性;
  • delattr(target, name, raising=True),删除属性;
  • setitem(dic, name, value),字典添加元素;
  • delitem(dic, name, raising=True),字典删除元素;
  • setenv(name, value, prepend=None),设置环境变量;
  • delenv(name, raising=True),删除环境变量;
  • syspath_prepend(path),添加系统路径;
  • chdir(path),切换目录。

其中 raising 用于通知 pytest 在元素不存在时是否抛出异常;prepend 如果设置,环境变量将变为 value+prepend+ 。

下面使用保存配置文件示例说明 monkeypatch 的作用和使用。

假设我们需要切换某个服务到国内科大源以加速,有以下脚本用于修改配置文件 .conf.json:

# test_monkeypatch.py

def dump_config(config):
    path = os.path.expanduser('~/.conf.json')
    with open(path, 'w', encoding='utf-8') as wr:
        json.dump(config, wr, indent=4)


def test_config():
    dump_config(config)
    path = os.path.expanduser('~/.conf.json')
    expected = json.load(open(path, 'r', encoding='utf-8'))
    assert expected == config

似乎测试正常执行完全没有问题,但如果我们的家目录下恰好有这个配置文件并且维护了许多配置,运行测试将会覆盖原有配置,这太可怕了!

所以我们需要修改测试,最好能在临时目录里完成。但程序已经写死了文件路径,怎么办?

这种在运行时控制程序的功能就需要 monkeypatch 来实现,下面在测试过程中修改了环境变量:

# test_monkeypatch.py

def test_config_monkeypatch(tmpdir, monkeypatch):
    monkeypatch.setenv('HOME', tmpdir.mkdir('home'))

    dump_config(config)
    path = os.path.expanduser('~/.conf.json')
    expected = json.load(open(path, 'r', encoding='utf-8'))
    assert expected == config

现在测试会来临时目录中执行,但环境变量可能对系统有依赖,所以更好的解决方法能自己控制路径中 ~ 的替换,这次通过改变 os.path.expanduser 的行为来实现:

# test_monkeypatch.py

def test_config_monkeypatch2(tmpdir, monkeypatch):
    fake_home = tmpdir.mkdir('home')
    monkeypatch.setattr(os.path, 'expanduser',
                        lambda x: x.replace('~', str(fake_home)))
    dump_config(config)
    path = os.path.expanduser('~/.conf.json')
    expected = json.load(open(path, 'r', encoding='utf-8'))
    assert expected == config
recwarn

recwarn 用于捕获程序中 warnings 产生的警告。

# test_recwarn.py

def warn():
    warnings.warn('Deprecated function', DeprecationWarning)


def test_warn(recwarn):
    warn()
    assert len(recwarn) == 1
    w = recwarn.pop()
    assert w.category == DeprecationWarning

此外,pytest 可以使用 pytest.warns() 捕获警告:

def test_warn2():
    with pytest.warns(None) as warnings:
        warn()

    assert len(warnings) == 1
    w = warnings.pop()
    assert w.category == DeprecationWarning

失败重试

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

pytest自带失败重跑机制,在运行中传入参数即可。

–lf 参数:运行上次失败的用例,如果没有则全部运行。

–ff参数:运行全部用例,上次失败的优先运行。

运行–lf参数:
在这里插入图片描述
再次运行:只执行了之前错误的用例。
在这里插入图片描述
以上的重跑都需要再次执行,不能体现无人值守以及真正的自动化,所以使用pytest-rerunfailures

失败重试依赖pytest-rerunfailures插件

pip install pytest-rerunfailures

用例失败再重跑一次,需要再命令行加参数–reruns
reruns参数有2个用法:

--reruns=RERUNS RERUNS是失败重跑的次数,默认为0
--reruns-delay=RERUNS_DELAY  RERUNS_DELAY是失败后间隔多少s重新执行。
pytest --reruns 1 -html=report.html --self-contained-html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值