每天进步一点点,关注我们哦,每天分享测试技术文章
本文章出自【码同学软件测试】
码同学公众号:自动化软件测试
码同学抖音号:小码哥聊软件测试
1.数据驱动框架设计
1.框架结构
common: 这是一个package,主要用来存储所有的底层代码封装
logs: 这是一个目录,主要用来存放日志文件
report: 这是一个目录,里边的data表示测试结果数据,里边的html表示测试报告,注意这两个目录都是每次执行测试时自动生成的
testcases: 这是一个目录,主要用来存储excel文件,excel文件里是接口测试的相关数据
conftest.py: 重写pytest自带的一个内置函数的,统一管理自定义fixture的
pytest.ini: pytest相关的配置参数
run.py: 是整个框架执行的入口
2.excel数据规则设计
按照一定的维度进行分类,每个分类可以当做一个sheet工作表
全局变量
主要用来管理我们的公共数据
变量名称变量值
hosthttp://82.xxx74.xx:xxxx
username18866668888
password123456
接口默认参数
通常在一个项目中,参数如果很多的时候,我们针对测试用例去传递数据就会很麻烦,所以我们针对每个接口的默认参数数据进行单独管理,在测试时只需要针对当前测试用例传递你要测试的某个字段值即可,其他字段统统来自于默认参数
填写参数的规则:对于接口参数可能会有多种类型,表单的,查询的,json的,文件的等等
表单类型时:
{
"data":{
"xxx":"xxjsdhdh"
}
}
查询参数:
{
"params":{
"xxx":"xxjsdhdh"
}
}
json参数:
{
"json":{
"xxx":"xxjsdhdh"
}
}
混合参数,比如既有表单又有查询:
{
"params":{
"xxx":"xxjsdhdh"
},
"data":{
"ddd":"ddff"
}
}
接口名称默认参数
登录{ "data":{ "username":"${username}", "password":"${password}" } }
新增客户{ "json":{ "entity": { "customer_name": "沙陌001", "mobile": "18729399607", "telephone": "01028375678", "website": "http://mtongxue.com/", "next_time": "2022-05-12 00:00:00", "remark": "这是备注", "address": "北京市,北京城区,昌平区", "detailAddress": "霍营地铁口", "location": "", "lng": "", "lat": "" } } }
新建联系人{ "json":{ "entity": { "name": "沙陌001联系人", "customer_id":"${customerId}", "mobile": "18729399607", "telephone": "01028378782", "email": "[email protected]", "post": "采购部员工", "address": "这是地址", "next_time": "2022-05-10 00:00:00", "remark": "这是备注" } } }
新建产品{ "json":{ "entity": { "name": "python全栈自动化", "category_id": 23, "num": "98888", "price": "6980", "description": "接口/web/app/持续集成" } } }
测试集合管理
测试集合管理 主要是为了控制要执行哪些测试集合,以及测试集合执行的顺序
测试集合名称:对应的就是某个测试集合的sheet工作表名称
是否执行:只有值是y时才会被执行,其他值不会被执行
测试集合名称是否执行
新增客户接口测试集合y
新建联系人接口测试集合y
新建产品接口测试集合y
测试集合
每个测试集合在excel里是一个单独的sheet工作表,他负责某个模块或者某个接口相关的测试用例数据管理,一个测试集合中是可以存在多个测试用例的
序号:仅仅只是个标识,没啥作用
用例名称:一个用例可能会有多个接口的先后调用,在excel里一行数据就是针对一个接口的调用,多行数据就是多个接口的调用,如果一个用例需要用多行数据,那么这几行的用例名称保持一致
接口名称:该列主要是为了和接口默认参数中的接口名称进行关联,通过接口名称得到该接口对应的默认参数,然后再根据测试数据来决定参数是什么
接口地址:表示接口地址,在接口地址里域名几乎都是相同的,或者说是公共的,所以我们将域名作为了公共变量进行存储,那么在这里需要调用公共变量域名,调用方式${host},host就是公共变量中的一个变量
请求方式:get/post/put/delete
接口头信息:指的就是headers,对于一个接口来说不一定有特殊的头信息,那么就不填,如果有需要按照如下格式进行填写,以json格式字符串的方式:
token是要从登录接口的返回值中提取的,提取之后保存到一个变量中,咱们这里保存的变量名称叫做token,所以在这里引用了变量token,引用方式就是${token}
{
"Admin-Token":"${token}"
}
假如特殊的头信息有多个,写法就是在json字符串中继续追加键值对,比如:
{
"Admin-Token":"${token}",
"Content-Type":"application/json"
}
测试数据:指的是在接口发起调用时传递的你要测试的某个数据,对于一个接口来说参数有很多,但是我们每次测试时,可能只是针对一两个参数进行测试,可以借助之前所学的通过jsonpath去匹配某些参数,并且替换他们的值
设计思路:
一个json格式的字符串,其中参数类型分为data、json、params、files
其中的key是你要替换的目标参数对应的jsonpath,value就是该参数对应的新值,也就是测试数据
{
"json":{
"$.entity.customer_id":999999999,
}
}
如果要替换多个参数:
{
"json":{
"$.entity.name":"自动化${{cur_timestamp()}}",
"$.entity.num":"${timestamp}}"
}
}
响应提取:响应提取是为了从当前接口的返回值中提取某些信息,保存在变量中,以便后续接口要使用时进行变量引用,这也是我们常说的关联
比如每个接口都要用到token,token是登录接口产生的,所以登录的响应提取里要写提取内容,规则是以json格式的字符串作为标准格式,其中key是要保存的变量名称,value是要提取的参数对应的jsonpath表达式
{
"token":"$.Admin-Token"
}
期望响应状态码:http响应状态码期望值
期望响应信息:表示我们要针对接口的响应信息中的某些参数进行断言,设计规则如下:
依然是json格式的字符串,最外层是一个列表,里边套的是多个字典,一个字典就是一个参数的断言。
每个字典的格式是必须包括两个键值对,一个actual表示实际值的key,实际值的value是参数对应的jsonpath表达式,一个expect表示期望值的key,期望值的value是期望内容
[
{
"actual":"$.code",
"expect":500,
},
{
"actual":"$.msg",
"expect":"产品编号已存在,请校对后再添加!",
}
]
2.数据驱动框架底层代码实现
1.创建项目
依赖于设计去创建项目结构
2.excel数据读取
在common这个package下创建一个python文件,叫做testcase_util.py
# !/usr/bin python3
# encoding: utf-8 -*-
# @file : testcase_util.py
# @author : 沙陌 Matongxue_2
# @Time : 2022-05-10 11:27
# @Copyright: 北京码同学
importopenpyxl
# 读取全局变量sheet工作表
defget_variables(wb):
sheet_data=wb['全局变量']
variables= {}# 用来存储读到的变量,名称是key,值是value
lines_count=sheet_data.max_row# 获取总行数
forlinrange(2,lines_count+1):
key=sheet_data.cell(l,1).value
value=sheet_data.cell(l,2).value
variables[key] =value
returnvariables
defget_api_default_params(wb):
sheet_data=wb['接口默认参数']
api_default_params= {}# 用来存储读到的变量,名称是key,值是value
lines_count=sheet_data.max_row# 获取总行数
forlinrange(2,lines_count+1):
key=sheet_data.cell(l,1).value
value=sheet_data.cell(l,2).value
api_default_params[key] =value
returnapi_default_params
# 获取要执行的测试集合名称
defget_casesuitename(wb):
sheet_data=wb['测试集合管理']
lines_count=sheet_data.max_row# 获取总行数
cases_suite_name= []# 用来存储要执行的测试集合名称
forlinrange(2,lines_count+1):
flag=sheet_data.cell(l,2).value
ifflag=='y':
suite_name=sheet_data.cell(l,1).value
cases_suite_name.append(suite_name)
returncases_suite_name
# 需要根据要执行的测试集合名称来读取对应的测试用例数据
defread_testcases(wb,suite_name):
sheet_data=wb[suite_name]
lines_count=sheet_data.max_row# 获取总行数
cols_count=sheet_data.max_column# 获取总列数
"""
规定读出来的测试数据存储结构如下:
{
“新增客户正确”:[
['apiname','接口地址','请求方式','头信息',....],
['apiname','接口地址','请求方式','头信息',....],
],
"新增客户失败-用户名为空":[
['apiname','接口地址','请求方式','头信息',....]
],
"新增客户失败-手机号格式不正确":[
['apiname','接口地址','请求方式','头信息',....]
]
}
"""
cases_info= {}#用来存储当前测试集合中的所有用例信息的
forlinrange(2,lines_count+1):
case_name=sheet_data.cell(l,2).value# 测试用例名称
lines= []# 用来存储当前行测试数据的
forcinrange(3,cols_count+1):
cell=sheet_data.cell(l,c).value# 当前单元格数据
ifcell==None:# 处理空单元格
cell=''
lines.append(cell)
# 判断当前用例名称是否已存在于cases_info中
# 如果不存在,那就是直接赋值
# 否则就是在原来的基础上追加
ifcase_namenotincases_info:
cases_info[case_name] = [lines]
else:
cases_info[case_name].append(lines)
returncases_info
# 整合所有要执行的测试用例数据,将其转成pytest参数化需要的数据结构格式
defget_all_testcases(wb):
"""
整合后的数据结构是
[
['新增客户接口测试集合','新增客户正确',[[],[]]],
['新增客户接口测试集合','新增客户失败-用户名为空',[[],[]]],
['新增客户接口测试集合','新增客户失败-手机号格式不正确',[[],[]]],
['新建产品接口测试集合','新建产品正确',[[],[]]],
['新建产品接口测试集合','新建产品失败-产品编码重复',[[],[]]],
]
:param wb:
:return:
"""
test_data= []# 用来存储所有测试数据
# 获取所有要执行的测试集合名称
cases_suite_name=get_casesuitename(wb)
forsuite_nameincases_suite_name:
# 遍历读取每个要执行的测试集合sheet工作表中的测试用例数据
cur_cases_info=read_testcases(wb,suite_name)# 是个字典
forkey,valueincur_cases_info.items():
# key实际上就是测试用例名称,value实际上测试用例多行数据信息
case_info= [suite_name,key,value]
test_data.append(case_info)
returntest_data
if__name__=='__main__':
wb=openpyxl.load_workbook('../testcases/CRM系统接口测试用例.xlsx')
# print(get_variables(wb))
# print(get_api_default_params(wb))
# print(get_casesuitename(wb))
# print(read_testcases(wb,'新增客户接口测试集合'))
print(get_all_testcases(wb))
3.接口调用底层方法封装
在common目录下创建一个client.py,写上如下代码
# !/usr/bin python3
# encoding: utf-8 -*-
# @file : client.py
# @author : 沙陌 Matongxue_2
# @Time : 2022-05-11 10:01
# @Copyright: 北京码同学
importjsonpath
importrequests
session=requests.session()
classRequestsClient:
defsend(self,url,method,**kwargs):
try:
self.resp=session.request(url=url,method=method,**kwargs)
exceptBaseExceptionase:
raiseBaseException(f'接口发起异常:{e}')
returnself.resp
# 针对jsonpath的数据提取封装一个方法
# 第一个参数指的是你要匹配的数据的jsonpath表达式
# 第二个指的是你想返回匹配到的第几个,默认是0返回第一个
defextract_resp(self,json_path,index=0):
# 注意有的接口是没有返回信息的,返回信息是空的
text=self.resp.text# 获取返回信息的字符串形式
iftext!='':
resp_json=self.resp.json()# 获取响应信息的json格式
# 如果能匹配到值,那么res就是个列表
# 如果匹配不到res就是个False
res=jsonpath.jsonpath(resp_json,json_path)
ifres:
ifindex<0:
# 如果index小于0 ,我认为你要匹配到的所有结果
returnres
else:
returnres[index]
else:
print('没有匹配到任何东西')
else:
raiseBaseException('接口返回信息为空,无法提取')
if__name__=='__main__':
client=RequestsClient()
client.send(url='http://82.156.74.26:9099/login',
method='post',
data={'username':'18866668888','password':'123456'})
print(client.extract_resp('Admin-Token'))
4.辅助函数封装及引用定义
在我们测试时,有的参数并不能够写死,所以这个时候我们希望某个参数在每次执行时都是动态变化的,那么就需要我们封装一些辅助随机函数来帮我们完成数据的动态变化
在common目录下建一个util_func.py的文件,在其中写上我们需要用到的辅助函数
随机数生成我们可以用一个第三方库faker
# !/usr/bin python3
# encoding: utf-8 -*-
# @file : run.py
# @author : 沙陌 Matongxue_2
# @Time : 2022-05-10 11:24
# @Copyright: 北京码同学
importhashlib
importtime
fromfakerimportFaker
fake=Faker(locale='zh_CN')
defrdm_phone_number():
returnfake.phone_number()
defcur_timestamp():#到毫秒级的时间戳
returnint(time.time()*1000)
defcur_date():# 2021-12-25
returnfake.date_between_dates()
defcur_date_time():# 2021-12-25 10:07:33
returnfake.date_time_between_dates()
defrdm_date(pattern='%Y-%m-%d'):
returnfake.date(pattern=pattern)
defrdm_date_time():
returnfake.date_time()
defrdm_future_date_time(end_date):
returnfake.future_datetime(end_date=end_date)
defmd5(data):
data=str(data)
returnhashlib.md5(data.encode('UTF-8')).hexdigest()
if__name__=='__main__':
print(rdm_phone_number())
print(rdm_date())
print(rdm_date_time())
print(cur_date())
print(cur_timestamp())
print(cur_date_time())
print(rdm_future_date_time('+60d'))
print(md5('123456'))
在excel中需要用到动态函数时,调用规则是${{md5(123456)}} 再比如${{rdm_future_date_time(+60d)}}
5.excel中动态数据的正则替换
正则表达式基本规则
基本规则: https://baike.baidu.com/item/正则表达式/1700215
在线正则调试:https://tool.oschina.net/regex
代码封装
在testcase_util.py文件中增加如下代码:
# 该方法是针对excel中数据中存在动态变量时,进行变量识别以及替换的
defregx_sub(string,vars_dict):
res=re.findall(r'\$\{([A-Za-z_]+?)\}',string)
forvar_nameinres:
# print(var_name)
value=vars_dict[var_name]
# print(value)
# 得到变量对应的值value,然后用字符串替换的方法替换
string=string.replace(f'${{{var_name}}}',str(value))
returnstring
# 针对excel数据中存在动态函数调用时,使用正则匹配并执行函数完成数据替换
defregx_func_exec(string):
res=re.findall(r'\$\{\{(.+?)\((.*?)\)\}\}',string)
forfunc_methodinres:
print(func_method)
func_name=func_method[0]# 函数名称
func_params=func_method[1]# 函数参数
# 使用python中的反射机制来实现函数执行
ifhasattr(util_func,func_name):
# 如果该函数在util_func这个文件中,那么我就去得到该函数对象
f_method=getattr(util_func,func_name)
iffunc_params=='':
value=f_method()
else:
value=f_method(func_params)
string=string.replace(f'${{{{{func_name}({func_params})}}}}',str(value))
else:
raiseBaseException(f'{func_name}不存在')
returnstring
# 统一的针对数据做动态变量和动态函数的替换
defregx_sub_data(string,vars_dict):
string=regx_sub(string,vars_dict)
string=regx_func_exec(string)
returnstring
6.统一测试方法封装
针对框架去封装一个执行测试的入口,这个入口是一个基于pytest参数化的测试用例,在run.py中实现
补充json数据替换的方法
在testcases_util.py中增加如下方法:
defupdate_value_to_json(json_object,json_path,new_value):
json_path_expr=parse(json_path)
formatchinjson_path_expr.find(json_object):
path=match.path# 这是获取到匹配结果的路径
ifisinstance(path,Index):
match.context.value[match.path.index] =new_value
elifisinstance(path,Fields):
match.context.value[match.path.fields[0]] =new_value
returnjson_object
补充内置变量timestamp
在testcases_util.py中修改下述方法
defget_variables(wb):
sheet_data=wb['全局变量']
variables= {}# 用来存储读到的变量,名称是key,值是value
lines_count=sheet_data.max_row# 获取总行数
forlinrange(2,lines_count+1):
key=sheet_data.cell(l,1).value
value=sheet_data.cell(l,2).value
variables[key] =value
# 增加一个内置变量,叫时间戳,注意这个时间戳是当前测试一运行就会产生,产生之后在当前测试未完成之前不管调用
# 多少次,都是一致的
variables['timestamp'] =cur_timestamp()
returnvariables
run.py里的代码
# !/usr/bin python3
# encoding: utf-8 -*-
# @file : run.py
# @author : 沙陌 Matongxue_2
# @Time : 2022-05-10 11:24
# @Copyright: 北京码同学
免费领取码同学软件测试课程笔记+超多学习资料+完整视频+最新面试题,可以转发文章+ 私信「码同学666」获取资料哦
importopenpyxl
importpytest
fromcommon.clientimportRequestsClient
fromcommon.testcase_utilimportget_all_testcases,get_variables,get_api_default_params,regx_sub_data,\
update_value_to_json
wb=openpyxl.load_workbook('testcases/CRM系统接口测试用例.xlsx')
# 获取所有的测试用例数据
test_data=get_all_testcases(wb)
variables=get_variables(wb)# 获取所有的公共变量,也用来存储测试过程中产生的动态变量
api_default_params=get_api_default_params(wb)# 获取所有接口的默认参数数据
@pytest.mark.parametrize('suite_name,case_name,case_info_list',test_data)
deftest_run(suite_name,case_name,case_info_list):
# 创建一个接口调用的对象
client=RequestsClient()
# case_info_list 是多个接口的数据,是一个列表
forcase_infoincase_info_list:
# case_info 其实也是一个列表,表示excel某一行的测试数据,从接口名称开始往后
# ['登录', '${host}/login', 'post', '', '', '{\n"token":"$.Admin-Token"\n}', 200, '[\n{\n"actual":"$.code",\n"expect":0\n}\n]']
kwargs= {'verify':False}#verify表示忽略https的证书
api_name=case_info[0]# 接口名称
url=case_info[1]# 接口名称
url=regx_sub_data(url,variables)# 处理url中的动态变量及动态函数调用
method=case_info[2]# 接口请求方式
headers=case_info[3]# 接口头信息
ifheaders!='':
headers=regx_sub_data(headers,variables)
headers=eval(headers)# 将json格式的字符串转换成字典
kwargs['headers'] =headers
# 测试数据并不是接口发起时真正的全部参数,需要根据用户填入的要测试的数据和该接口对应的默认数据进行替换以及组合来达到
# 请求数据
api_default_param=api_default_params[api_name]# 获取当前行的接口对应的默认数据
ifapi_default_param!='':
api_default_param=regx_sub_data(api_default_param,variables)
api_default_param=eval(api_default_param)
test_params=case_info[4]# 测试数据
iftest_params!='':
test_params=regx_sub_data(test_params,variables)
test_params=eval(test_params)
# 解析测试数据,通过jsonpath去替换默认参数中的数据
# 逻辑是遍历测试数据,判断测试数据中是哪种参数类型(data/params/json/files),根据参数类型去替换默认数据的对应的部分
if'json'intest_params:
"""
{
"$.entity.name":"联系人${{cur_timestamp()}}",
}
"""
forjson_path,new_valueintest_params['json'].items():
api_default_param['json'] =update_value_to_json(api_default_param['json'],json_path,new_value)
if'data'intest_params:
forjson_path,new_valueintest_params['data'].items():
api_default_param['data'] =update_value_to_json(api_default_param['data'],json_path,new_value)
if'params'intest_params:
forjson_path,new_valueintest_params['params'].items():
api_default_param['params'] =update_value_to_json(api_default_param['params'],json_path,new_value)
if'files'intest_params:
forjson_path,new_valueintest_params['files'].items():
api_default_param['files'] =update_value_to_json(api_default_param['files'],json_path,new_value)
test_params=api_default_param
# 整合完成测试数据和默认数据之后,将他们分别存储kwargs中
if'json'intest_params:
kwargs['json'] =test_params['json']
if'data'intest_params:
kwargs['data'] =test_params['data']
if'params'intest_params:
kwargs['params'] =test_params['params']
if'files'intest_params:
kwargs['files'] =test_params['files']
resp=client.send(url=url,method=method,**kwargs)# 发起请求
expect_status=case_info[6]# 期望的响应状态码
assertresp.status_code==expect_status
# print(resp.text)
extract_resp=case_info[5]# 响应提取
ifextract_resp!='':
extract_resp=eval(extract_resp)
"""
{
"token":"$.Admin-Token"
}
"""
forkey,valueinextract_resp.items():
# key就是提取后要保存的变量名称
# value是你要提取的目标字段对应的jsonpath表达式
res=client.extract_resp(value)
variables[key] =res
expect_resp=case_info[7]# 期望的响应信息
ifexpect_resp!='':
expect_resp=regx_sub_data(expect_resp,variables)
expect_resp=eval(expect_resp)
"""
[
{
"actual":"$.code",
"expect":500,
},
{
"actual":"$.msg",
"expect":"产品编号已存在,请校对后再添加!",
}
]
"""
forexpect_infoinexpect_resp:
json_path=expect_info['actual']
actual_res=client.extract_resp(json_path)
expect_res=expect_info['expect']
pytest.assume(actual_res==expect_res,f'期望是{expect_res},实际是{actual_res}')
if__name__=='__main__':
pytest.main()# 该方法会自动扫描当前项目中的pytest.ini,根据其中的配置进行执行
7.集成日志收集
日志收集的目的是在我们用例有失败时,可以帮助我们去追溯问题产生的原因。日志都要收集哪些信息呢?
主要收集接口发起以及接口响应的各项信息,在什么地方去集成日志可以收集到这些信息?
修改client.py中的代码如下:
# !/usr/bin python3
# encoding: utf-8 -*-
# @file : client.py
# @author : 沙陌 Matongxue_2
# @Time : 2022-05-11 10:01
# @Copyright: 北京码同学
importlogging
importjsonpath
importrequests
session=requests.session()
classRequestsClient:
def__init__(self):
self.logger=logging.getLogger(__class__.__name__)
defsend(self,url,method,**kwargs):
self.logger.info(f'接口地址是:{url}')
self.logger.info(f'接口请求方式是:{method}')
forkey,valueinkwargs.items():
self.logger.info(f'接口的{key}是:{value}')
try:
self.resp=session.request(url=url,method=method,**kwargs)
self.logger.info(f'接口响应状态码是:{self.resp.status_code}')
self.logger.info(f'接口响应信息是:{self.resp.text}')
exceptBaseExceptionase:
self.logger.exception('接口发起异常')
raiseBaseException(f'接口发起异常:{e}')
returnself.resp
# 针对jsonpath的数据提取封装一个方法
# 第一个参数指的是你要匹配的数据的jsonpath表达式
# 第二个指的是你想返回匹配到的第几个,默认是0返回第一个
defextract_resp(self,json_path,index=0):
# 注意有的接口是没有返回信息的,返回信息是空的
text=self.resp.text# 获取返回信息的字符串形式
iftext!='':
resp_json=self.resp.json()# 获取响应信息的json格式
# 如果能匹配到值,那么res就是个列表
# 如果匹配不到res就是个False
res=jsonpath.jsonpath(resp_json,json_path)
ifres:
ifindex<0:
# 如果index小于0 ,我认为你要匹配到的所有结果
self.logger.info(f'通过{json_path}提取到的结果是:{res}')
returnres
else:
self.logger.info(f'通过{json_path}提取到的结果是:{res[index]}')
returnres[index]
else:
self.logger.warning(f'通过{json_path}没有提取到结果')
returnres
else:
self.logger.exception('接口返回信息为空,无法提取')
raiseBaseException('接口返回信息为空,无法提取')
if__name__=='__main__':
client=RequestsClient()
client.send(url='http://82.156.74.26:9099/login',
method='post',
data={'username':'18866668888','password':'123456'})
print(client.extract_resp('Admin-Token'))
配置pytest.ini文件
在pytest.ini文件中追加如下内容:
;critical > error > warning > info > debug
log_cli = true
log_cli_level = INFO
log_format = %(asctime) s [%(filename) s:%(lineno) s] [%(levelname) s] %(message) s
log_date_format=%Y-%m-%d %H:%M:%S
log_file = logs/test.log
log_cli = true: 表示日志要输出到控制台上
log_cli_level=INFO: 表示要收集的日志等级,日志分为critical > error > warning > info > debug
log_format = xxx:表示日志的格式
log_date_format = xxx: 表示日志中的时间格式
log_file = logs/test.log: 日志收集存储的文件,要注意他的路径
8.allure测试报告集成
收集pytest执行的测试结果数据
需要用到一个python的第三方库,叫做allure-pytest,所以先安装
在pytest.ini中追加allure结果数据收集的命令参数
addopts=-sv--alluredir./report/data--clean-alluredir
--alluredir ./report/data :表示收集到的测试结果存放在report/data目录中
--clean-alluredir :表示每次执行收集结果前都先清除上一次的结果
生成html报告
需要用到allure的命令行工具,命令行工具下载地址:
https://github.com/allure-framework/allure2/releases
如果无法访问,那么就下载我提供的allure-2.11.0.zip
下载之后解压即可,解压以后去配环境变量path,配如下路径
配完以后,在命令行中输入allure --version能看到版本号,就说明配置好了
记得重启pycharm,在pycharm进入终端输入如下命令:
allure generate ./report/data -o ./report/html
报告打开:
每次命令行输入命令比较麻烦,可以直接将生成命令集成在代码中,修改run.py中的main里代码如下:
if __name__ == '__main__':
pytest.main() # 该方法会自动扫描当前项目中的pytest.ini,根据其中的配置进行执行
os.system('allure generate ./report/data -o ./report/html --clean')
优化allure测试报告展示
增加测试用例的层级划分
修改run.py中的代码如下:
# !/usr/bin python3
# encoding: utf-8 -*-
# @file : run.py
# @author : 沙陌 Matongxue_2
# @Time : 2022-05-10 11:24
# @Copyright: 北京码同学
importos
importallure
importopenpyxl
importpytest
fromcommon.clientimportRequestsClient
fromcommon.testcase_utilimportget_all_testcases,get_variables,get_api_default_params,regx_sub_data,\
update_value_to_json
wb=openpyxl.load_workbook('testcases/CRM系统接口测试用例.xlsx')
# 获取所有的测试用例数据
test_data=get_all_testcases(wb)
variables=get_variables(wb)# 获取所有的公共变量,也用来存储测试过程中产生的动态变量
api_default_params=get_api_default_params(wb)# 获取所有接口的默认参数数据
@pytest.mark.parametrize('suite_name,case_name,case_info_list',test_data)
deftest_run(suite_name,case_name,case_info_list):
# 创建一个接口调用的对象
client=RequestsClient()
allure.dynamic.feature(suite_name)# 测试报告上会高于测试用例的层级展示
allure.dynamic.title(case_name)# 测试报告上表示测试用例的名称
# case_info_list 是多个接口的数据,是一个列表
forcase_infoincase_info_list:
# case_info 其实也是一个列表,表示excel某一行的测试数据,从接口名称开始往后
# ['登录', '${host}/login', 'post', '', '', '{\n"token":"$.Admin-Token"\n}', 200, '[\n{\n"actual":"$.code",\n"expect":0\n}\n]']
kwargs= {'verify':False}#verify表示忽略https的证书
api_name=case_info[0]# 接口名称
url=case_info[1]# 接口名称
url=regx_sub_data(url,variables)# 处理url中的动态变量及动态函数调用
method=case_info[2]# 接口请求方式
headers=case_info[3]# 接口头信息
ifheaders!='':
headers=regx_sub_data(headers,variables)
headers=eval(headers)# 将json格式的字符串转换成字典
kwargs['headers'] =headers
# 测试数据并不是接口发起时真正的全部参数,需要根据用户填入的要测试的数据和该接口对应的默认数据进行替换以及组合来达到
# 请求数据
api_default_param=api_default_params[api_name]# 获取当前行的接口对应的默认数据
ifapi_default_param!='':
api_default_param=regx_sub_data(api_default_param,variables)
api_default_param=eval(api_default_param)
test_params=case_info[4]# 测试数据
iftest_params!='':
test_params=regx_sub_data(test_params,variables)
test_params=eval(test_params)
# 解析测试数据,通过jsonpath去替换默认参数中的数据
# 逻辑是遍历测试数据,判断测试数据中是哪种参数类型(data/params/json/files),根据参数类型去替换默认数据的对应的部分
if'json'intest_params:
"""
{
"$.entity.name":"联系人${{cur_timestamp()}}",
}
"""
forjson_path,new_valueintest_params['json'].items():
api_default_param['json'] =update_value_to_json(api_default_param['json'],json_path,new_value)
if'data'intest_params:
forjson_path,new_valueintest_params['data'].items():
api_default_param['data'] =update_value_to_json(api_default_param['data'],json_path,new_value)
if'params'intest_params:
forjson_path,new_valueintest_params['params'].items():
api_default_param['params'] =update_value_to_json(api_default_param['params'],json_path,new_value)
if'files'intest_params:
forjson_path,new_valueintest_params['files'].items():
api_default_param['files'] =update_value_to_json(api_default_param['files'],json_path,new_value)
test_params=api_default_param
# 整合完成测试数据和默认数据之后,将他们分别存储kwargs中
if'json'intest_params:
kwargs['json'] =test_params['json']
if'data'intest_params:
kwargs['data'] =test_params['data']
if'params'intest_params:
kwargs['params'] =test_params['params']
if'files'intest_params:
kwargs['files'] =test_params['files']
resp=client.send(url=url,method=method,**kwargs)# 发起请求
expect_status=case_info[6]# 期望的响应状态码
assertresp.status_code==expect_status
# print(resp.text)
extract_resp=case_info[5]# 响应提取
ifextract_resp!='':
extract_resp=eval(extract_resp)
"""
{
"token":"$.Admin-Token"
}
"""
forkey,valueinextract_resp.items():
# key就是提取后要保存的变量名称
# value是你要提取的目标字段对应的jsonpath表达式
res=client.extract_resp(value)
variables[key] =res
expect_resp=case_info[7]# 期望的响应信息
ifexpect_resp!='':
expect_resp=regx_sub_data(expect_resp,variables)
expect_resp=eval(expect_resp)
"""
[
{
"actual":"$.code",
"expect":500,
},
{
"actual":"$.msg",
"expect":"产品编号已存在,请校对后再添加!",
}
]
"""
forexpect_infoinexpect_resp:
json_path=expect_info['actual']
actual_res=client.extract_resp(json_path)
expect_res=expect_info['expect']
pytest.assume(actual_res==expect_res,f'期望是{expect_res},实际是{actual_res}')
if__name__=='__main__':
pytest.main()# 该方法会自动扫描当前项目中的pytest.ini,根据其中的配置进行执行
os.system('allure generate ./report/data -o ./report/html --clean')
免费领取码同学软件测试课程笔记+超多学习资料+学习完整视频,可以关注我们公众号哦:自动化软件测试
本文著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。