基于数据驱动的接口自动化框架封装
每天进步一点点,关注我们哦,每天分享测试技术文章
本文章出自【码同学软件测试】
码同学公众号:自动化软件测试,领取资料可加:magetest
码同学抖音号:小码哥聊软件测试
1.数据驱动框架设计
1.框架结构
- common: 这是一个package,主要用来存储所有的底层代码封装
- logs: 这是一个目录,主要用来存放日志文件
- report: 这是一个目录,里边的data表示测试结果数据,里边的html表示测试报告,注意这两个目录都是每次执行测试时自动生成的
- testcases: 这是一个目录,主要用来存储excel文件,excel文件里是接口测试的相关数据
- conftest.py: 重写pytest自带的一个内置函数的,统一管理自定义fixture的
- pytest.ini: pytest相关的配置参数
- run.py: 是整个框架执行的入口
2.excel数据规则设计
按照一定的维度进行分类,每个分类可以当做一个sheet工作表
- 全局变量
- 主要用来管理我们的公共数据
变量名称 |
变量值 |
host |
http://82.xxx74.xx:xxxx |
username |
18866668888 |
password |
123456 |
- 接口默认参数
- 通常在一个项目中,参数如果很多的时候,我们针对测试用例去传递数据就会很麻烦,所以我们针对每个接口的默认参数数据进行单独管理,在测试时只需要针对当前测试用例传递你要测试的某个字段值即可,其他字段统统来自于默认参数
- 填写参数的规则:对于接口参数可能会有多种类型,表单的,查询的,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": "sdsdd@qq.com", "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: 北京码同学import openpyxl# 读取全局变量sheet工作表def get_variables(wb): sheet_data = wb['全局变量'] variables = {} # 用来存储读到的变量,名称是key,值是value lines_count = sheet_data.max_row # 获取总行数 for l in range(2,lines_count+1): key = sheet_data.cell(l,1).value value = sheet_data.cell(l,2).value variables[key] = value return variablesdef get_api_default_params(wb): sheet_data = wb['接口默认参数'] api_default_params = {} # 用来存储读到的变量,名称是key,值是value lines_count = sheet_data.max_row # 获取总行数 for l in range(2,lines_count+1): key = sheet_data.cell(l,1).value value = sheet_data.cell(l,2).value api_default_params[key] = value return api_default_params# 获取要执行的测试集合名称def get_casesuitename(wb): sheet_data = wb['测试集合管理'] lines_count = sheet_data.max_row # 获取总行数 cases_suite_name = [] # 用来存储要执行的测试集合名称 for l in range(2,lines_count+1): flag = sheet_data.cell(l,2).value if flag == 'y': suite_name = sheet_data.cell(l,1).value cases_suite_name.append(suite_name) return cases_suite_name# 需要根据要执行的测试集合名称来读取对应的测试用例数据def read_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 = {} #用来存储当前测试集合中的所有用例信息的 for l in range(2,lines_count+1): case_name = sheet_data.cell(l,2).value # 测试用例名称 lines = [] # 用来存储当前行测试数据的 for c in range(3,cols_count+1): cell = sheet_data.cell(l,c).value # 当前单元格数据 if cell == None: # 处理空单元格 cell = '' lines.append(cell) # 判断当前用例名称是否已存在于cases_info中 # 如果不存在,那就是直接赋值 # 否则就是在原来的基础上追加 if case_name not in cases_info: cases_info[case_name] = [lines] else: cases_info[case_name].append(lines) return cases_info# 整合所有要执行的测试用例数据,将其转成pytest参数化需要的数据结构格式def get_all_testcases(wb): """ 整合后的数据结构是 [ ['新增客户接口测试集合','新增客户正确',[[],[]]], ['新增客户接口测试集合','新增客户失败-用户名为空',[[],[]]], ['新增客户接口测试集合','新增客户失败-手机号格式不正确',[[],[]]], ['新建产品接口测试集合','新建产品正确',[[],[]]], ['新建产品接口测试集合','新建产品失败-产品编码重复',[[],[]]], ] :param wb: :return: """ test_data = [] # 用来存储所有测试数据 # 获取所有要执行的测试集合名称 cases_suite_name = get_casesuitename(wb) for suite_name in cases_suite_name: # 遍历读取每个要执行的测试集合sheet工作表中的测试用例数据 cur_cases_info = read_testcases(wb,suite_name) # 是个字典 for key,value in cur_cases_info.items(): # key实际上就是测试用例名称,value实际上测试用例多行数据信息 case_info = [suite_name,key,value] test_data.append(case_info) return test_dataif __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.接口调用底层方法封装
免费领取码同学软件测试课程笔记+超多学习资料+完整视频+面试题,可加微信:magetest
在common目录下创建一个client.py,写上如下代码
# !/usr/bin python3 # encoding: utf-8 -*- # @file : client.py # @author : 沙陌 Matongxue_2# @Time : 2022-05-11 10:01# @Copyright: 北京码同学import jsonpathimport requestssession = requests.session()class RequestsClient: def send(self,url,method,**kwargs): try: self.resp = session.request(url=url,method=method,**kwargs) except BaseException as e: raise BaseException(f'接口发起异常:{e}') return self.resp # 针对jsonpath的数据提取封装一个方法 # 第一个参数指的是你要匹配的数据的jsonpath表达式 # 第二个指的是你想返回匹配到的第几个,默认是0返回第一个 def extract_resp(self,json_path,index=0): # 注意有的接口是没有返回信息的,返回信息是空的 text = self.resp.text # 获取返回信息的字符串形式 if text != '': resp_json = self.resp.json() # 获取响应信息的json格式 # 如果能匹配到值,那么res就是个列表 # 如果匹配不到res就是个False res = jsonpath.jsonpath(resp_json,json_path) if res: if index < 0: # 如果index小于0 ,我认为你要匹配到的所有结果 return res else: return res[index] else: print('没有匹配到任何东西') else: raise BaseException('接口返回信息为空,无法提取')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: 北京码同学import hashlibimport timefrom faker import Fakerfake = Faker(locale='zh_CN')def rdm_phone_number(): return fake.phone_number()def cur_timestamp():#到毫秒级的时间戳 return int(time.time() * 1000)def cur_date():# 2021-12-25 return fake.date_between_dates()def cur_date_time():# 2021-12-25 10:07:33 return fake.date_time_between_dates()def rdm_date(pattern='%Y-%m-%d'): return fake.date(pattern=pattern)def rdm_date_time(): return fake.date_time()def rdm_future_date_time(end_date): return fake.future_datetime(end_date=end_date)def md5(data): data = str(data) return hashlib.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中数据中存在动态变量时,进行变量识别以及替换的def regx_sub(string,vars_dict): res = re.findall(r'\$\{([A-Za-z_]+?)\}',string) for var_name in res: # print(var_name) value = vars_dict[var_name] # print(value) # 得到变量对应的值value,然后用字符串替换的方法替换 string = string.replace(f'${{{var_name}}}',str(value)) return string# 针对excel数据中存在动态函数调用时,使用正则匹配并执行函数完成数据替换def regx_func_exec(string): res = re.findall(r'\$\{\{(.+?)\((.*?)\)\}\}',string) for func_method in res: print(func_method) func_name = func_method[0] # 函数名称 func_params = func_method[1] # 函数参数 # 使用python中的反射机制来实现函数执行 if hasattr(util_func,func_name): # 如果该函数在util_func这个文件中,那么我就去得到该函数对象 f_method = getattr(util_func,func_name) if func_params == '': value = f_method() else: value =f_method(func_params) string = string.replace(f'${{{{{func_name}({func_params})}}}}',str(value)) else: raise BaseException(f'{func_name}不存在') return string# 统一的针对数据做动态变量和动态函数的替换def regx_sub_data(string,vars_dict): string = regx_sub(string,vars_dict) string = regx_func_exec(string) return string
6.统一测试方法封装
针对框架去封装一个执行测试的入口,这个入口是一个基于pytest参数化的测试用例,在run.py中实现
- 补充json数据替换的方法
在testcases_util.py中增加如下方法:
def update_value_to_json(json_object,json_path,new_value): json_path_expr = parse(json_path) for match in json_path_expr.find(json_object): path = match.path # 这是获取到匹配结果的路径 if isinstance(path,Index): match.context.value[match.path.index] = new_value elif isinstance(path,Fields): match.context.value[match.path.fields[0]] = new_value return json_object
- 补充内置变量timestamp
在testcases_util.py中修改下述方法
def get_variables(wb): sheet_data = wb['全局变量'] variables = {} # 用来存储读到的变量,名称是key,值是value lines_count = sheet_data.max_row # 获取总行数 for l in range(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() return variables
- run.py里的代码
# !/usr/bin python3 # encoding: utf-8 -*- # @file : run.py # @author : 沙陌 Matongxue_2# @Time : 2022-05-10 11:24# @Copyright: 北京码同学import openpyxlimport pytestfrom common.client import RequestsClientfrom common.testcase_util import get_all_testcases, get_variables, get_api_default_params, regx_sub_data, \ update_value_to_jsonwb = 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)def test_run(suite_name,case_name,case_info_list): # 创建一个接口调用的对象 client = RequestsClient() # case_info_list 是多个接口的数据,是一个列表 for case_info in case_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] # 接口头信息 if headers!='': headers = regx_sub_data(headers,variables) headers = eval(headers) # 将json格式的字符串转换成字典 kwargs['headers'] = headers # 测试数据并不是接口发起时真正的全部参数,需要根据用户填入的要测试的数据和该接口对应的默认数据进行替换以及组合来达到 # 请求数据 api_default_param = api_default_params[api_name] # 获取当前行的接口对应的默认数据 if api_default_param != '': api_default_param = regx_sub_data(api_default_param,variables) api_default_param = eval(api_default_param) test_params = case_info[4] # 测试数据 if test_params != '': test_params = regx_sub_data(test_params,variables) test_params = eval(test_params) # 解析测试数据,通过jsonpath去替换默认参数中的数据 # 逻辑是遍历测试数据,判断测试数据中是哪种参数类型(data/params/json/files),根据参数类型去替换默认数据的对应的部分 if 'json' in test_params: """ { "$.entity.name":"联系人${{cur_timestamp()}}", } """ for json_path,new_value in test_params['json'].items(): api_default_param['json'] = update_value_to_json(api_default_param['json'],json_path,new_value) if 'data' in test_params: for json_path,new_value in test_params['data'].items(): api_default_param['data'] = update_value_to_json(api_default_param['data'],json_path,new_value) if 'params' in test_params: for json_path,new_value in test_params['params'].items(): api_default_param['params'] = update_value_to_json(api_default_param['params'],json_path,new_value) if 'files' in test_params: for json_path,new_value in test_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' in test_params: kwargs['json'] = test_params['json'] if 'data' in test_params: kwargs['data'] = test_params['data'] if 'params' in test_params: kwargs['params'] = test_params['params'] if 'files' in test_params: kwargs['files'] = test_params['files'] resp = client.send(url=url,method=method,**kwargs) # 发起请求 expect_status = case_info[6] # 期望的响应状态码 assert resp.status_code == expect_status # print(resp.text) extract_resp = case_info[5] # 响应提取 if extract_resp != '': extract_resp = eval(extract_resp) """ { "token":"$.Admin-Token" } """ for key,value in extract_resp.items(): # key就是提取后要保存的变量名称 # value是你要提取的目标字段对应的jsonpath表达式 res = client.extract_resp(value) variables[key] = res expect_resp = case_info[7] # 期望的响应信息 if expect_resp != '': expect_resp = regx_sub_data(expect_resp,variables) expect_resp = eval(expect_resp) """ [ { "actual":"$.code", "expect":500, }, { "actual":"$.msg", "expect":"产品编号已存在,请校对后再添加!", } ] """ for expect_info in expect_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: 北京码同学import loggingimport jsonpathimport requestssession = requests.session()class RequestsClient: def __init__(self): self.logger = logging.getLogger(__class__.__name__) def send(self,url,method,**kwargs): self.logger.info(f'接口地址是:{url}') self.logger.info(f'接口请求方式是:{method}') for key,value in kwargs.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}') except BaseException as e: self.logger.exception('接口发起异常') raise BaseException(f'接口发起异常:{e}') return self.resp # 针对jsonpath的数据提取封装一个方法 # 第一个参数指的是你要匹配的数据的jsonpath表达式 # 第二个指的是你想返回匹配到的第几个,默认是0返回第一个 def extract_resp(self,json_path,index=0): # 注意有的接口是没有返回信息的,返回信息是空的 text = self.resp.text # 获取返回信息的字符串形式 if text != '': resp_json = self.resp.json() # 获取响应信息的json格式 # 如果能匹配到值,那么res就是个列表 # 如果匹配不到res就是个False res = jsonpath.jsonpath(resp_json,json_path) if res: if index < 0: # 如果index小于0 ,我认为你要匹配到的所有结果 self.logger.info(f'通过{json_path}提取到的结果是:{res}') return res else: self.logger.info(f'通过{json_path}提取到的结果是:{res[index]}') return res[index] else: self.logger.warning(f'通过{json_path}没有提取到结果') return res else: self.logger.exception('接口返回信息为空,无法提取') raise BaseException('接口返回信息为空,无法提取')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 > debuglog_cli = truelog_cli_level = INFOlog_format = %(asctime) s [%(filename) s:%(lineno) s] [%(levelname) s] %(message) slog_date_format=%Y-%m-%d %H:%M:%Slog_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: 北京码同学import osimport allureimport openpyxlimport pytestfrom common.client import RequestsClientfrom common.testcase_util import get_all_testcases, get_variables, get_api_default_params, regx_sub_data, \ update_value_to_jsonwb = 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)def test_run(suite_name,case_name,case_info_list): # 创建一个接口调用的对象 client = RequestsClient() allure.dynamic.feature(suite_name) # 测试报告上会高于测试用例的层级展示 allure.dynamic.title(case_name) # 测试报告上表示测试用例的名称 # case_info_list 是多个接口的数据,是一个列表 for case_info in case_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] # 接口头信息 if headers!='': headers = regx_sub_data(headers,variables) headers = eval(headers) # 将json格式的字符串转换成字典 kwargs['headers'] = headers # 测试数据并不是接口发起时真正的全部参数,需要根据用户填入的要测试的数据和该接口对应的默认数据进行替换以及组合来达到 # 请求数据 api_default_param = api_default_params[api_name] # 获取当前行的接口对应的默认数据 if api_default_param != '': api_default_param = regx_sub_data(api_default_param,variables) api_default_param = eval(api_default_param) test_params = case_info[4] # 测试数据 if test_params != '': test_params = regx_sub_data(test_params,variables) test_params = eval(test_params) # 解析测试数据,通过jsonpath去替换默认参数中的数据 # 逻辑是遍历测试数据,判断测试数据中是哪种参数类型(data/params/json/files),根据参数类型去替换默认数据的对应的部分 if 'json' in test_params: """ { "$.entity.name":"联系人${{cur_timestamp()}}", } """ for json_path,new_value in test_params['json'].items(): api_default_param['json'] = update_value_to_json(api_default_param['json'],json_path,new_value) if 'data' in test_params: for json_path,new_value in test_params['data'].items(): api_default_param['data'] = update_value_to_json(api_default_param['data'],json_path,new_value) if 'params' in test_params: for json_path,new_value in test_params['params'].items(): api_default_param['params'] = update_value_to_json(api_default_param['params'],json_path,new_value) if 'files' in test_params: for json_path,new_value in test_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' in test_params: kwargs['json'] = test_params['json'] if 'data' in test_params: kwargs['data'] = test_params['data'] if 'params' in test_params: kwargs['params'] = test_params['params'] if 'files' in test_params: kwargs['files'] = test_params['files'] resp = client.send(url=url,method=method,**kwargs) # 发起请求 expect_status = case_info[6] # 期望的响应状态码 assert resp.status_code == expect_status # print(resp.text) extract_resp = case_info[5] # 响应提取 if extract_resp != '': extract_resp = eval(extract_resp) """ { "token":"$.Admin-Token" } """ for key,value in extract_resp.items(): # key就是提取后要保存的变量名称 # value是你要提取的目标字段对应的jsonpath表达式 res = client.extract_resp(value) variables[key] = res expect_resp = case_info[7] # 期望的响应信息 if expect_resp != '': expect_resp = regx_sub_data(expect_resp,variables) expect_resp = eval(expect_resp) """ [ { "actual":"$.code", "expect":500, }, { "actual":"$.msg", "expect":"产品编号已存在,请校对后再添加!", } ] """ for expect_info in expect_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')
免费领取码同学软件测试课程笔记+超多学习资料+学习完整视频 ☞ 可加:magetest/关注码同学公众号:自动化软件测试
本文著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。