资料来源: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