PyTest介绍:
PyTest是一个测试用例的管理框架,在UnitTest基础上做的一个全面的升级。
集成度更高,而且更加自由的一个测试框架,全程都是基于指令的形式运行。
PyTest-uniTest-RobotFramework-airtest
PyTest环境部署:
pip install pytest
输入pytest检查安装成功,输入pytest会自动编译,执行当前目录及子目录下是否有符合pytest规则的文件
pytest遵寻规则:
1、pytest默认运行以test开头或者结尾的文件
2、pytest默认不输出任何打印信息,如果要看打印信息,需要在运行时添加’-s’指令
3、pytest默认寻找当前路径下所有文件与子文件夹中以test开头或结尾的文件夹、文件、函数作为识别对象(逐层寻找,若文件并非以test开头,函数已test开头,则不会识别到)
4、多条指令一同运行时,需要通过空格进行区分,在main函数中,是通过’ , '进行分割
5、类的命名必须以Test开头,且不能有__init__初始化方法
6、方法必须以test_开头
7、断言必须使用assert
import pytest
class Test1:
def test_01(self):
assert 1==1
def test_02(self):
assert 1==1
if __name__ == "__main__":
# -s 打印出预期结果与实际结果不一致的print里面的内容,
pytest.main(['-s','test_case.py::test_02'])
(venv) D:\workspace\code\pytestStudyCode\class01\test_demo>pytest -s test_case.py::test_02
console中的指令:
指令含义 | 指令 |
---|---|
用于详细显示日志信息 | -v |
测试结果的简单统计 | -rA |
只显示整体测试结果 | -q |
帮助 | -h |
显示print打印信息 | -s |
输出html格式报告 | –html=path输出路径 |
生成log报告 | –resultlog=./log.txt |
生成xml报告 | –junitxml=./log.xml |
当错误达到num时,停止测试 | –maxfail=num |
只运行有MARKEXPR(自定义)标记的测试 | -m MARKEXPR |
生成简略的指定需求的报告 | -r option |
pytest执行命令:
指令含义 | 指令 |
---|---|
运行所有test测试函数 | pytest |
运行test_mod.py下的所有测试函数 | pytest test_mod.py |
运行指定路径somepath下的测试函数 | pytest somepath |
运行符合表达式stringexpr的函数,如stringexpr为“Myclass and not me”,则会运行TMyclass.tree,不会运行TMyclass.haha_me_haha | pytest -k stringexpr |
运行test_mod.py文件下的test_func测试函数 | pytest test_mod.py::test_func |
pytest中的setup 和 teardown
1、一般通过一个配置文件直接进行管理:配置文件名一定要是conftest.py
2、在conftest中定义预置函数(用于前期的数据准备),使用@pytest.fixture(),使用@pytest.mark.usefixtures()作为装饰器(无法获取到fixture的返回值)
方法:fixture(scope=“function”, params=None, autouse=False, ids=None, name=None)
params:(list类型)提供参数数据,供调用标记方法的函数使用;一个可选的参数列表,它将导致多个参数调用fixture功能和所有测试使用它
param的使用:
import pytest
def data():
return ['01','02','03']
@pytest.fixture(params=data())
def get_data(request):
return request.param
def test_05(get_data):
print("\ntest_"+get_data)
if __name__ == "__main__":
pytest.main(['-s','test_case03.py::test_05'])
结果依次传参输出:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case03.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Workspace\projects\Exercise\pytest_test, configfile: pytest.ini
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collected 3 items
test_case03.py
test_01
.
test_02
.
test_03
.
============================== 3 passed in 0.05s ==============================
confest.py文件:
import pytest
'''
这是pytest中setup_class和teardown_class的配置文件
'''
@pytest.fixture()
def xuzhu():
print('虚竹生病了但是很强')
@pytest.fixture()
def xuzhu1():
return 1
测试用例:
import pytest
def test_01(xuzhu):
print('test01')
def test_02(xuzhu1):
assert xuzhu1 == 1, '失败'
print('test02')
@pytest.mark.usefixtures("xuzhu")
def test_o3():
print("test_03")
if __name__ == "__main__":
# pytest.main(['-s','test_case.py::test_02'])
pytest.main(['-s','test_case.py'])
fixture 中参数scope定义的4种等级: session:在每个session中只执行一次 module:在每个模块中只执行一次 class:在每个class中只执行一次 function:在每个函数中只执行一次
示例:
conftest.py预置文件,mike函数的scope为session:
import pytest
@pytest.fixture(scope='session')
def mike():
print("mike is godfather")
@pytest.fixture()
def mike2():
print("mike is miserable!!!")
test_case.py文件中的函数test_01和test_02都调用了mike这个预置函数:
import pytest
class Test01:
@pytest.mark.website
def test_01(self,mike):
print("test_01")
assert 1==1
@pytest.mark.website
def test_02(self,mike):
print("test02")
assert 2==2
if __name__ == "__main__":
pytest.main(['-s','test_case.py'])
运行结果,只运行了一次mike函数:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Workspace\projects\Exercise\pytest_test
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collected 3 items
test_case.py mike is godfather
---setup_class---
test_01
.test02
若将mike的scope设置为function,则运行两次mike函数:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Workspace\projects\Exercise\pytest_test
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collected 3 items
test_case.py ---setup_class---
mike is godfather
test_01
.mike is godfather
test02
setup:在测试函数或类之前执行,完成准备工作,如;数据库连接、测试数据、打开文件等,setup_class、setup_function、setup_module
teardown:在测试函数或类结束后执行,完成收尾工作,如数据库断开、资源回收等,teardown_class、teardown_function、teardown_module
示例:
import pytest
class Test01:
def setup_class(self):
print("---setup_class---")
def teardown_class(self):
print("---teardown_class----")
@pytest.mark.website
def test_01(self,mike):
print("test_01")
assert 1==1
@pytest.mark.website
def test_02(self,mike):
print("test02")
assert 2==2
def test_03(self):
print("test_03")
assert 3==3
if __name__ == "__main__":
pytest.main(['-s','test_case.py'])
运行结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Workspace\projects\Exercise\pytest_test
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collected 3 items
test_case.py ---setup_class---
mike is godfather
test_01
.mike is godfather
test02
.test_03
.---teardown_class----
pytest.mark对test进行分类执行
1、只运行带有特定标记的测试函数,如website标记
import pytest
class Test01:
@pytest.mark.website
def test_01(self):
print("test_01")
assert 1==1
@pytest.mark.website
def test_02(self):
print("test02")
assert 2==2
def test_03(self):
print("test_03")
assert 3==3
运行及结果:
D:\Workspace\projects\Exercise>pytest -s -m "website" test_case.py
========================================================================================================== test session starts ===========================================================================================================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Workspace\projects\Exercise
plugins: allure-pytest-2.9.43
collected 3 items / 1 deselected / 2 selected
test_case.py test_01
.test02
2、不运行带有特定标记的函数,如website标记
运行结果:
D:\Workspace\projects\Exercise>pytest -s -m "not website" test_case.py
========================================================================================================== test session starts ===========================================================================================================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Workspace\projects\Exercise
plugins: allure-pytest-2.9.43
collected 3 items / 2 deselected / 1 selected
test_case.py test_03
pytest.ini配置文件
markers :指定运行的markers标记文件
python_files :指定运行特定命名的python文件(多个类型文件用空格隔开)
python_classes:指定运行特定命名的class文件
python_functions:指定运行特定命名的函数
addopts:命令行参数(使用空格分开)
testpaths:只执行该设置下的文件夹下的test
norecursedirs:不执行所设置的文件夹下的test
跳过某些测试函数 skipif
跳过测试类,则在类外引用下列标签
@pytest.mark.skipif(condition, reason=None)
condition:不执行的情况(条件)
reason:不执行的理由
直接跳过执行测试函数:@pytest.mark.skip(reason=" ")
跳过测试函数:pytest.skip()
根据某些条件跳过模块中的所有测试用例如: pytestmark = pytest.mark.skipif(sys.platform == “win32”,reason=“tests for linux only”)
标记某些测试函数为失败函数 xfail
标记为测试预期失败,直接跳过该函数:
@pytest.mark.xfail(condition=None, reason=None, raises=None, run=True, strict=False)
condition:标记为失败的条件
reason:标记为失败的理由
强制标记为失败,并且不再执行测试函数中的代码:
pytest.xfail()
import pytest
def test_06():
print("\n----test_06----")
@pytest.mark.xfail(2>1,reason='标记失败')
def tast_07():
print("-------test_08---------")
def test_08():
if 1<2:
pytest.xfail("failing!")
print("---------test_07----------")
if __name__ == "__main__":
pytest.main(['-s','test_case04.py'])
结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case04.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\Workspace\projects\Exercise\pytest_test, configfile: pytest.ini
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collected 2 items
test_case04.py
----test_06----
.x
======================== 1 passed, 1 xfailed in 0.17s =========================
函数数据参数化
方便测试函数对参数的调用:
@pytest.mark.parametrize(argnames,argvalues, indirect=False, ids=None, scope=None)
argnames:参数名
argvalues:参数对应值,可传多个值,类型必须为list [(values1,values2,…),(value1,value2,…)]
1、参数传递
单参/多参传递:
import pytest
'''
parametrize
'''
class Test02:
def setup_class(self):
print("---------setup_class----------")
def teardown_class(self):
print("------------teardown_class-------------")
# 传递单参数
@pytest.mark.parametrize("a",[3,6])
def test_09(self,a):
print("a = %d" % a)
assert a%3 == 0
# 传递多参数
@pytest.mark.parametrize('a,b',[(0,3),[1,2]])
def test_10(self,a,b):
print("%d + %d = %d" % (a,b,a+b))
assert a+b == 3
if __name__ == "__main__":
pytest.main(['-s','-v','test_case05.py::Test02::test_10'])
函数返回值传递参数
import pytest
def test_data():
return [(1, 2), (0, 3)]
# 函数返回值传递参数
@pytest.mark.parametrize('a,b', test_data())
def test_11(a, b):
print("%d + %d = %d" % (a, b, a + b))
assert a + b == 3
if __name__ == "__main__":
pytest.main(['-s','-v',"test_case06.py::test_11"])
ids的使用
def divsion(a,b):
return int(a/b)
@pytest.mark.parametrize('a,b,c',[(3,1,3),(0,2,0),(6,7,0)],ids=['整除','被除数为0','非整除'])
def test_12(a,b,c):
s = divsion(a,b)
assert s == c
if __name__ == "__main__":
pytest.main(['-s','-v',"test_case06.py::test_12"])
结果:产生乱码:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case06.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- E:\Python\Python37\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.9', 'Platform': 'Windows-10-10.0.19041-SP0', 'Packages': {'pytest': '6.2.4', 'py': '1.10.0', 'pluggy': '0.13.1'}, 'Plugins': {'allure-pytest': '2.9.43', 'html': '3.1.1', 'metadata': '1.11.0'}, 'JAVA_HOME': 'E:\\Java\\jdk'}
rootdir: D:\Workspace\projects\Exercise\pytest_test, configfile: pytest.ini
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collecting ... collected 3 items
test_case06.py::test_12[\u6574\u9664] PASSED
test_case06.py::test_12[\u88ab\u9664\u6570\u4e3a0] PASSED
test_case06.py::test_12[\u975e\u6574\u9664] PASSED
============================== 3 passed in 0.06s ==============================
Process finished with exit code 0
解决:在pytest.ini文件中加入:disable_test_id_escaping_and_forfeit_all_rights_to_community_support = True
结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case06.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- E:\Python\Python37\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.9', 'Platform': 'Windows-10-10.0.19041-SP0', 'Packages': {'pytest': '6.2.4', 'py': '1.10.0', 'pluggy': '0.13.1'}, 'Plugins': {'allure-pytest': '2.9.43', 'html': '3.1.1', 'metadata': '1.11.0'}, 'JAVA_HOME': 'E:\\Java\\jdk'}
rootdir: D:\Workspace\projects\Exercise\pytest_test, configfile: pytest.ini
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collecting ... collected 3 items
test_case06.py::test_12[整除] PASSED
test_case06.py::test_12[被除数为0] PASSED
test_case06.py::test_12[非整除] PASSED
============================== 3 passed in 0.03s ==============================
Process finished with exit code 0
3、叠加使用
def divsion(a,b):
return int(a/b)
# 叠加使用
@pytest.mark.parametrize('a',[3,0,6])
@pytest.mark.parametrize('b,c',[(1,3),(2,0),(7,0)])
def test_13(a,b,c):
s = divsion(a,b)
assert s == c
if __name__ == "__main__":
pytest.main(['-s','-v',"test_case06.py::test_13"])
结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case06.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- E:\Python\Python37\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.9', 'Platform': 'Windows-10-10.0.19041-SP0', 'Packages': {'pytest': '6.2.4', 'py': '1.10.0', 'pluggy': '0.13.1'}, 'Plugins': {'allure-pytest': '2.9.43', 'html': '3.1.1', 'metadata': '1.11.0'}, 'JAVA_HOME': 'E:\\Java\\jdk'}
rootdir: D:\Workspace\projects\Exercise\pytest_test, configfile: pytest.ini
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collecting ... collected 9 items
test_case06.py::test_13[1-3-3] PASSED
test_case06.py::test_13[1-3-0] FAILED
test_case06.py::test_13[1-3-6] FAILED
test_case06.py::test_13[2-0-3] FAILED
test_case06.py::test_13[2-0-0] PASSED
test_case06.py::test_13[2-0-6] FAILED
test_case06.py::test_13[7-0-3] PASSED
test_case06.py::test_13[7-0-0] PASSED
test_case06.py::test_13[7-0-6] PASSED
================================== FAILURES ===================================
python traceback
traceback object 用于打印异常信息出错路径,可帮助我们更清楚地定位出程序的异。打印出traceback object信息必须使用sys.exc_info()这个函数 ,可通过下面的代码运行结果看出,sys.exc_info()返回的是一个元组,分别为异常的类型、异常值、异常的路径对象,而这个路径对象就是traceback object:
import sys
def fun():
raise Exception("--error--")
def operate():
try:
fun()
except Exception as e :
type,value,traceback_object = sys.exc_info()
print("type: \t%s,\nvalue:\t%s,\ntraceback_object:\t%s"%(type,value,traceback_object))
if __name__ == "__main__":
operate()
结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case07.py
type: <class 'Exception'>,
value: --error--,
traceback_object: <traceback object at 0x000001D8DD99B7C8>
Process finished with exit code 0
traceback模块输出异常信息的方法有很多种:print_tb、print_exception、print_exc、format_exc等
print_tb
traceback.print_tb(tb[, limit[, file]])
tb:traceback对象,通过sys.exc_info()获取到的
limit:设置打印stack trace的层级,不设置则默认打印所有
file:设置打印的输出流的,可以为文件,也可以是stdout之类的file-like object。如果不设或为None,则输出到sys.stderr
import traceback
import sys
def fun():
raise Exception("--error--")
def operate():
try:
fun()
except Exception as e :
type,value,traceback_object = sys.exc_info()
print("type: \t%s,\nvalue:\t%s,\ntraceback_object:\t%s"%(type,value,traceback_object))
traceback.print_tb(traceback_object)
if __name__ == "__main__":
operate()
结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case07.py
type: <class 'Exception'>,
value: --error--,
traceback_object: <traceback object at 0x000001B17D11B888>
File "D:/Workspace/projects/Exercise/pytest_test/test_case07.py", line 9, in operate
fun()
File "D:/Workspace/projects/Exercise/pytest_test/test_case07.py", line 6, in fun
raise Exception("--error--")
Process finished with exit code 0
print_exception
traceback.print_exception(etype, value, tb[, limit[, file]])
etype:sys.exc_info()返回的异常类型
value:sys.exc_info()返回的异常信息值
tb:traceback对象,通过sys.exc_info()获取到的
limit:设置打印stack trace的层级,不设置则默认打印所有
file:设置打印的输出流的,可以为文件,也可以是stdout之类的file-like object。如果不设或为None,则输出到sys.stderr
注:当异常为SyntaxError时,会有"^"来指示语法错误的位置
print_exc
traceback.print_exc([limit[, file]])
limit:设置打印stack trace的层级,不设置则默认打印所有
file:设置打印的输出流的,可以为文件,也可以是stdout之类的file-like object。如果不设或为None,则输出到sys.stderr
注:可自动获取到sys.exc_info()返回的三个值
format_exc
直接提字符串形式返回错误结果
logger = logging.getLogger("test_traceback")
def fun():
raise Exception("--error--")
def operate():
try:
fun()
except Exception as e :
type,value,traceback_object = sys.exc_info()
print("type: \t%s,\nvalue:\t%s,\ntraceback_object:\t%s"%(type,value,traceback_object))
# traceback.print_tb(traceback_object)
logger.error(traceback.format_exc(limit=1))
if __name__ == "__main__":
operate()
结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case07.py
Traceback (most recent call last):
File "D:/Workspace/projects/Exercise/pytest_test/test_case07.py", line 11, in operate
fun()
Exception: --error--
type: <class 'Exception'>,
value: --error--,
traceback_object: <traceback object at 0x00000269D495BB88>
Process finished with exit code 0
pytest-指令输出traceback:
含义 | 指令 |
---|---|
traceback 输出当前变量值 | - -showlocals |
输出当前变量值(shortcut) | - - l |
(default) ‘long’ tracebacks for the first and last | –tb=auto |
exhaustive, informative traceback formatting | –tb=long |
shorter traceback format | –tb=short |
only one line per failure | –tb=line |
python标准二进制格式 | Python standard library formatting |
不输出异常路径 | –tb=no |
执行失败时跳转PDB
使用pdb,在测试失败时开启调试:
含义 | 指令 |
---|---|
每次遇到失败都跳转到 PDB | - -pdb |
第一次遇到失败就跳转到 PDB,结束测试执行 | -x --pdb |
只有前三次失败跳转到 PDB | –pdb --maxfail=3 |
pdb调试符:
指令含义 | 指令 |
---|---|
输出变量 expr 的值 | p/print expr |
美化输出 expr 的值 | pp expr |
列出错误并显示错误之前和之后的5行代码 | l/list |
列出错误,并显示指定行号之间的代码 | l/lsit begin, end |
打印当前函数的所有参数和变量 | a/args |
移动到堆栈的上一层 | u/up |
移动到堆栈的下一层 | d/down |
退出当前调试会话(也会退出测试会话) | q/quit |
更多pdb用法,参考:https://docs.python.org/3/library/pdb.html
设置断点
设置断点后,只会关闭当前test输出的执行,不会影响其它test:
import pdb
pdb.set_trace()
示例:
import pdb
import pytest
def test_09():
return 3
def test_10():
a = 1
b = 2
d = test_09()
pdb.set_trace()
c = a+b
assert c == d
if __name__ == "__main__":
pytest.main(['-v','-s','test_case08.py::test_10'])
运行了test_09(),test_10()断点停止运行下一步,结果:
E:\Python\Python37\python.exe D:/Workspace/projects/Exercise/pytest_test/test_case08.py
============================= test session starts =============================
platform win32 -- Python 3.7.9, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- E:\Python\Python37\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.9', 'Platform': 'Windows-10-10.0.19041-SP0', 'Packages': {'pytest': '6.2.4', 'py': '1.10.0', 'pluggy': '0.13.1'}, 'Plugins': {'allure-pytest': '2.9.43', 'html': '3.1.1', 'metadata': '1.11.0'}, 'JAVA_HOME': 'E:\\Java\\jdk'}
rootdir: D:\Workspace\projects\Exercise\pytest_test, configfile: pytest.ini
plugins: allure-pytest-2.9.43, html-3.1.1, metadata-1.11.0
collecting ... collected 1 item
test_case08.py::test_10
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB set_trace >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> d:\workspace\projects\exercise\pytest_test\test_case08.py(13)test_10()
-> c = a+b
(Pdb) p d
3
获取用例执行数据
pytest --durations=10
生成JunitXML格式的结果文件
pytest --junitxml=path
禁用插件
如禁用docttest插件:
pytest -p no:doctest
从python代码中调用pytest
其它
1、多进程运行cases:pip install -U pytest-xdist
运行模式:pytest test_se.py -n NUM
Num为并发的进程数
2、生成html测试报告:pip install pytest-html
运行模式:pytest --html=./report.html
在当前目录会生成assets文件夹和report.html文件