Menu: python pytest测试实战 3
参考链接
hook函数:https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
conftest.py
完成数据共享
不同目录可以创建自己的conftest.py文件
作用域:
对当前同级目录下所有的文件及包下的所有测试文件,测试方法生效,
如果同级目录下没有conftest.py,会找上级目录或者上上级目录的
conftest.py里的fixture方法。不会找兄弟目录。
#conftest.py文件内容
import pytest
@pytest.fixture()
def login(request): #在方法参数写request
print("登录方法")
print(request.param) #方法体里面使用request.param接收参数
print("teardown")
#test_demo1文件内容
import pytest
@pytest.mark.parametrize('login',[ #如果conftest里面的方法使用了request参数,则必须进行传参
('username','password')
],indirect=True)
def test_demo(login): #方法接收方法名字
print("test demo1")
#执行结果为
登录方法
('username', 'password')
teardown
test demo1
pytest 常用插件
pip install pytest-ordering 控制用例的执行顺序
pip install pytest-dependency 控制用例的依赖关系
pip install pytest-xdist 分布式并发执行测试用例
pip install pytest-rerunfailures 失败重跑
pip install pytest-assume 多重较验
pip intall pytest-html 测试报告
pip install pytest-rerunfailures 失败重跑
场景:
测试失败后要重新运行n次,要在重新运行之间添加延迟时间,间隔n秒再运行
安装:
pip install pytest-rerunfailures
执行:
pytest -vs --reruns 5 test_class.py #指定文件里面所有运行的测试用例
pytest -vs --reruns 5 --reruns-delay 1
@pytest.mark.flaky(reruns=5, reruns_delay=2) #指定到单个用例
失败的fixture与setup_class也会被执行
不要与setup_class,setup_module方法使用,一个测试方法上面不要有多个装饰器
pip install pytest-assume 多重校验
#区别于assert,assert失败一条,下面的用例不会接着走,而第三方的pytest-assume就算执行失败也会往下走下去
import pytest
@pytest.mark.parametrize(('x', 'y'), [(1, 1), (1, 0), (0, 1)])
def test_simple_assume(x, y):
pytest.assume(x == y)
pytest.assume(True) #如果使用的是assert,
#上面的用例'pytest.assume(x == y)'未执行通过不会执行该条用例
#如果使用的是三方插件assume,就算上面用例未执行通过,也会执行该条用例
pip install pytest-dependency 控制用例的依赖关系
import pytest
@pytest.mark.dependency()
@pytest.mark.xfail(reason="deliberate fail")
def test_a():
assert False
@pytest.mark.dependency()
def test_b():
pass
#如果test_a 用例成功,test_c会执行
#如果test_a 用例失败,test_c会跳过
#depends=[] 列表里面加入依赖的测试用例名称
@pytest.mark.dependency(depends=["test_a"])
def test_c():
pass
@pytest.mark.dependency(depends=["test_b"])
def test_d():
pass
@pytest.mark.dependency(depends=["test_b", "test_c"])
def test_e():
pass
#执行结果为:
test_dependency.py::test_a XFAIL (deliberate fail) [ 20%]
test_dependency.py::test_b PASSED [ 40%]
test_dependency.py::test_c SKIPPED (test_c depends on test_a) [ 60%]
Skipped: test_c depends on test_a
test_dependency.py::test_d PASSED [ 80%]
test_dependency.py::test_e SKIPPED (test_e depends on test_c) [100%]
Skipped: test_e depends on test_c
=================== 2 passed, 2 skipped, 1 xfailed in 0.07s ===================
pip install pytest-xdist 分布式并发执行测试用例
分布式并发执行测试用例原则:
用例之间是独立的,用例之间没有依赖关系,用例可以完全独立运行
用例执行没有顺序,随机顺序都能正常执行
每个用例都能重复运行,运行结果不影响其他用例
安装及运行:
pip install pytest-xdist
pytest -n 3 #表示三个并发,运行所有用例的时间会降低
#用例多时效果明显,多进程并发执行,同时支持allure
pytest.ini
自定义测试用例的编写规则,
要放在执行命令的同级目录,通常建议放在项目的根目录,执行的时候也要在项目的根目录下执行。
[pytest]
markers = add
div
python_files= check_* test_* #自定义文件修改后可识别 check_* test_* 开头的文件
python_classes = Check_* Test_*
python_functions = aaa_* test_*
addopts=-vs --alluredir=./result
norecursedirs = result logs datas test_demo* 运行时忽略某些文件夹
设计测试用例的原则
不建议测试用例设计顺序
让测试用例尽量的简单,
Menu: python pytest测试实战 4
数据驱动
测试数据的数据驱动,测试步骤不变,测试数据提出来 yaml
测试步骤的数据驱动
这些数据可以从excel,csv,yaml,json远程数据库读取来的数据,yaml文件可以进行注释
测试步骤的数据驱动
如果多条用例,执行用例时取yaml数据里面不同的数据进行执行
#calc.yml文件内容 路径为:datas/calc.yml
add:
int1:
- 1
- 2
- 5
int2:
- 2
- 4
- 6
bigint: [100,200,300]
float: [0.2,0.3,0.5]
minus:
- -1
- -2
- -3
div:
int1:
- 2
- 2
- 1
int2:
- 4
- 4
- 1
bigint: [200,200,1]
float: [1,2,0.5]
minus:
- -1
- -2
- -3
#add.yml文件内容
- add
- div
#测试步骤的数据驱动
# 测试文件
import pytest
import yaml
from pythoncode.calc import Calculator
with open('datas/calc.yml',encoding='utf-8') as f: #运行编码展示有问题,读取文件也需要修改编码格式
datas = yaml.safe_load(f) #如果多条用例,执行用例时取yaml数据里面不同的数据进行执行
addkeys = datas['add'].keys() #使用add 数据 datas['add'].keys()多维数组
addvalues = datas['add'].values()
divkeys = datas['div'].keys() #如果多条用例,执行用例时取yaml数据里面不同的数据进行执行
divvalues = datas['div'].values() #使用div 数据
def get_steps():
with open('steps/add.yml') as f:
steps = yaml.safe_load(f)
print(steps)
return steps
cal = Calculator() #实例化对象
#不同的用例使用不同的方法
def steps(a,b,result):
steps1 = get_steps()
for step in steps1:
if step == 'add':
assert result == cal.add(a,b) #使用add方法
elif step == 'div':
assert result == cal.div(a,b) #使用div方法
@pytest.mark.parametrize('a,b,result',addvalues,ids=addkeys)
def test_add(a,b,result):
steps(a,b,result)
class TestCalc:
# setup_class, teardown_class每个类里面 执行前后分别 执行
def setup_class(self):
self.cal = Calculator() #在类里面实例化,类里面需要使用对象时一定要在类里面实例化
print("类级别 setup")
def teardown_class(self):
print("类级别 teardown")
# @pytest.mark.parametrize('a,b,result',
# [[1, 2, 5],
# [2, 4, 6],
# [100, 200, 300],
# [0.2, 0.3, 0.5],
# [-1, -2, -3]],ids=['int1','int2','bigint','float','负数']) #ids参数增加可读性,给测试的数据取别名,别名也可以在allure中进行展示
@pytest.mark.parametrize('a,b,result',addvalues,ids=addkeys) #通过yaml文件读取上面注释内容,使用yaml文件add数据
def test_add2(self,a,b,result): #类里面方法要使用self
steps(a,b,result)
@pytest.mark.parametrize('a,b,result', divvalues, ids=divkeys) #通过yaml文件读取上面注释内容,使用yaml文件div数据
def test_div(self, a, b, result):
assert result == self.cal.div(a, b)
#注意事项:yml文件编写
以下为示例
#@pytest.mark.parametrize('a,b,result',[
# [1,2,3],
# [100,200,300],
# [0.01,0.03,0.04],
# [-1,-2,-3]],ids = ['int1','bigint','float','minus'])
add:
-
- 1
- 2
- 3
-
- 100
- 200
- 300
- [0.01,0.02,0.03]
- [-1,-2,-3]
div:
-
- 1
- 2
- 3
-
- 100
- 200
- 300
- [0.01,0.02,0.03]
- [-1,-2,-3]
#结果为: #没有key 'list' object has no attribute 'keys'
{
add:
[ [ 1, 2, 3 ],
[ 100, 200, 300 ],
[ 0.01, 0.02, 0.03 ],
[ -1, -2, -3 ] ],
div:
[ [ 1, 2, 3 ],
[ 100, 200, 300 ],
[ 0.01, 0.02, 0.03 ],
[ -1, -2, -3 ] ] }
=================================================
add:
int:
- 1
- 2
- 3
bigint:
- 100
- 200
- 300
float: [0.01,0.02,0.03]
minus: [-1,-2,-3]
div:
int:
- 1
- 2
- 3
bigint:
- 100
- 200
- 300
float: [0.01,0.02,0.03]
minus: [-1,-2,-3]
#结果为:
{
add:
{
int: [ 1, 2, 3 ],
bigint: [ 100, 200, 300 ],
float: [ 0.01, 0.02, 0.03 ],
minus: [ -1, -2, -3 ] },
div:
{
int: [ 1, 2, 3 ],
bigint: [ 100, 200, 300 ],
float: [ 0.01, 0.02, 0.03 ],
minus: [ -1, -2, -3 ] } }
===============================================
int:
- 1
- 2
- 3
bigint:
- 100
- 200
- 300
float: [0.01,0.02,0.03]
minus: [-1,-2,-3]
#结果为:
{
int: [ 1, 2, 3 ],
bigint: [ 100, 200, 300 ],
float: [ 0.01, 0.02, 0.03 ],
minus: [ -1, -2, -3 ] }
Pytest高级用法
Pytest插件:
插件可以改变pytest行为,可用的hook函数有很多
https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
Pytest插件加载方式:
内置plugin:
从代码内部的_pytest目录加载 #内部目录加载
外部插件(第三方插件):
通过setuptools entry points机制发现的第三方插件 #前面介绍过第三方插件
conftest.py存放的本地插件:(重点) #通过conftest.py文件自定义
自动模块发现机制
pytest --trace-config 查看当前pytest中所有的plugin(带有hook方法的文件)
hook(钩子)函数定制和扩展插件 #重点
conftest.py:本地的插件库,存放fixture函数或者hook函数作用于该文件所在的目录及其所有的子目录
#hook(钩子)函数定制和扩展插件
Pytest编写自己的插件(1)
pytest_collection_modifyitems 收集上来的测试用例实现定制化功能
解决问题:
自定义用例的执行顺序
解决编码问题(中文的测试用例名称)
自动添加标签
#hook钩子函数官方文档链接:https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
#conftest.py文件 使用钩子函数定制和扩展插件
#conftest.py文件内容:
def pytest_collection_modifyitems(
session: "Session", config: "Config", items: List["Item"]
) -> None:
print(items) #查看items测试用例数据
print(len(items))
#倒序执行 items里面的测试用例
items.reverse() #自定义用例的执行顺序
#含有中文的测试用例名称,改写编码格式 #conftest.py文件通过hook函数修改编码后仍然展示错误,需要修改读取文件编码格式
for item in items: #循环遍历,针对items下面的每一条用例
item.name = item.name.encode('utf-8').decode('unicode-escape')
item._nodeid = item.nodeid.encode('utf-8').decode('unicode-escape')
#给用例添加mark标签
if 'add' in item.nodeid: #nodeid是整个用例名称
item.add_marker(pytest.mark.add) #如果用例名称包含add,则给该条用例加上add标签
elif 'div' in item.nodeid:
item.add_marker(pytest.mark.div) #如果用例名称包含div,则给该条用例加上div标签
用例失败重跑
#用例失败重跑
用例失败重跑,在命令行执行 #pytest帮助文档命令参数
--lf, --last-failed 只重新运行上次运行失败的用例(或如果没有失败的话会全部跑)
--ff, --failed-first 运行所有测试,但首先运行上次运行失败的测试(这可能会重新测试,从而导致重复的fixture setup/teardown)
Pytest编写自己的插件(2)
pytest_addoption为pytest命令行增加自定义参数,每个pytest 到这个hook方法
解决问题:
命令行添加一个参数
#不同环境获取不同的测试数据
parser: 用户命令行参数与ini文件值的解析器
#命令行添加一个参数
###处理命令后传来的参数,设置成fixture,将test环境、dev环境或者其他环境分别处理,获取想要的不同环境下的测试数据###
def pytest_addoption(parser):
mygroup = parser.getgroup("hogwarts") #group 将下面所有的 option都展示在这个group下。
mygroup.addoption("--env", #注册一个命令行选项
default='test', #默认参数是test环境
dest='env',
help='set your run env' #展示说明
)
#上面内容设定后在conftest同级目录命令行运行pytest --help可查看自定义的命令
@pytest.fixture(scope='session')
def cmdoption(request): #解析参数
return request.config.getoption("--env", default='test')
#conftest文件内容:
#不同环境获取不同的测试数据
#命令行添加一个参数
#parser: 用户命令行参数与ini文件值的解析器
###处理命令后传来的参数,设置成fixture,将test环境、dev环境或者其他环境分别处理,获取想要的不同环境下的测试数据###
def pytest_addoption(parser):
mygroup = parser.getgroup("hogwarts") # group 将下面所有的 option都展示在这个group下。
mygroup.addoption("--env", # 注册一个命令行选项
default='test', # 默认参数是test环境
dest='env',
help='set your run env' # 展示说明
)
'''
上面内容设定后在conftest同级目录命令行运行pytest --help可查看自定义的命令
'''
@pytest.fixture(scope='session')
def cmdoption(request): # 解析参数
myenv = request.config.getoption("--env", default='test')
if myenv == 'test':
datapath = 'datas/test/data.yml'
elif myenv == 'dev':
datapath = 'datas/dev/data.yml'
with open(datapath,encoding='utf-8') as f:
datas = yaml.safe_load(f)
port = datas['env']['port'] #yml数据为字典,通过字典的方式取值即可
ip = datas['env']['ip']
return port,ip
#测试文件test_env.py文件内容
# 测试用例通过传入 fixture方法,获取 测试数据/ 开发数据
def test_case(cmdoption):
print("测试环境验证")
port, ip = cmdoption
print(f"环境ip : {ip} , port:{port}")
url = 'http://' + str(ip) + ":" + str(port) #int类型需要转换为字符串进行拼接
# requests.get(url)
print(url)
#执行结果为:
测试环境验证
环境ip : 10.10.2.1 , port:8888
http://10.10.2.1:8888
Pytest编写自己的插件(3)
pytest_generate_tests 可以实现自定义动态参数化方案或者扩展
解决问题:参数化简化
重写hook函数
def pytest_generate_tests(metafunc:"Metafunc") -> None:
if "param" in metafunc.fixturenames: #测试数据需要用到该函数时,需要注意参数名称和conftest.py里面的参数param保持一致
metafunc.parametrize("param",metafunc.module.par_to_test, # 测试数据要与par_to_test名称保持一致
ids=metafunc.module.case,scope='function') # 测试数据要与case名称保持一致
#conftest文件内容:
#通过 方法动态的生成测试用例
def pytest_generate_tests(metafunc:"Metafunc") -> None:
if "param" in metafunc.fixturenames:
metafunc.parametrize("param",metafunc.module.mydatas, #metafunc.module.datas 测试数据要与mydatas名称保持一致
ids=metafunc.module.myids,scope='function') #metafunc.module.myids 测试数据要与myids名称保持一致
#test_param文件内容:
# mydatas = [[1,2,3],[0.1,0.2,0.3]]
# myids = ['整数','浮点']
import yaml
with open('datas/param.yml',encoding='utf-8') as f: #使用'utf-8'让编译器识别中文
datas = yaml.safe_load(f)
# myids 和mydatas 要与conftest.py 勾子函数里面的
# metafunc.module.mydatas, ids=metafunc.module.myids 保持一致
mydatas = datas.values()
myids = datas.keys()
def test_params(param): #测试数据需要用到该函数时,需要注意参数名称和conftest.py里面的参数param保持一致
print("参数化简化,动态生成测试用例")
print(param)
#datas/param.yml文件内容:
# mydatas = [[1,2,3],[0.1,0.2,0.3]]
# myids = ['整数','浮点']
整数:
-
- 1
- 2
- 3
浮点数:
-
- 0.1
- 0.2
- 0.3
#执行结果为:
参数化简化,动态生成测试用例
[[0.1, 0.2, 0.3]]
参数化简化,动态生成测试用例
[[1, 2, 3]]
#判断小数
#场景:
浮点型进行除、减的算法时,比如结果是3.5,断言结果是3.50000000004
处理规则:
1.decimal 结果一定要和decimal进行比较
2.hamcrest close_to