什么叫做多接口的动态参数处理?
测试数据都放在excel中管理,每个接口如果有提取的数据就直接放在excel里写好,到时候直接读取出来做响应的提取操作即可。
-
电商项目: 购物车 token proId skuID等 需要执行多个接口 获取返回值;
-
业务流: 前面过很多步骤 【中间步骤都是单独的一个接口的请求】
-
以上情况 都不太适合用conftest夹具测试使用。因为前置很多 处理比较麻烦。
-
参考postman的处理方法。
- 先执行前置登录-- 正常接口测试
- 执行之后,提取数据-- 存在环境变量— 共享的 后面每个接口都可以调用数据,变量
- 后面接口调用 – {{变量}} === 这个是{{}}是一个占位符,用你从环境变量里取到值 替换掉这个位置的数据 【Jmeter - ${}】
-
设计的思路:业务流: 登录-搜索-进入详情页-添加购物车-查询购物车-结算-提交订单
- 先把业务流的接口用例都写出来,在excel表格里统一管理表格设计好之后,我们需要用代码来实现: 提取数据---->存储环境变量—> 查找占位符 —> 替换变量的一系列操作。我们一步一步完成。
- 1、关联的接口 需要提取的数据也提取出来: 加一个提取响应字段在excel里,用jsonpath提取
- 2、代码提取出来后,保存在环境变量里 == 类 动态属性
- 3、其他接口要用的位置用占位符表示,后面用代码替换成为 -环境变量里存的值。
- 先把业务流的接口用例都写出来,在excel表格里统一管理表格设计好之后,我们需要用代码来实现: 提取数据---->存储环境变量—> 查找占位符 —> 替换变量的一系列操作。我们一步一步完成。
-
第一步:提取出来需要的响应字段的值 - 用jsonpath
思路如下:- 1、先从excel标题读取【提取响应结果数据】这列; – 读取出来是字符串
- 2、转化为字典-- json.loads()反序列化
- 3、分别渠道这个提取表达式里的 key 和value ,key是字段名,value是jsonpath表达式。 – 用for循环遍历
- 4、从接口的响应结果里使用这个jsonpath表达式提取数据。
-
第二步:将响应字段存储到环境变量中(键-值,可以有多组): 数据读取出来后,要存起来: 全局共享给所有用例可用。
- 用类的动态属性存储提取是的数据。
- 类和类的属性 存出 环境变量的变量名 和值。 == setattr getattr
-
第四步:用占位符包裹要替换的变量,然后用环境变量里的数据替换占位符位置的变量名。
- 难点:代码如何自动识别这些要替换的数据?–用占位符 ; 占位符包裹的变量名出来? — 正则表达式匹配
如果能识别出来,我们后续其实就比较简单了:- 1、取出来占位符中的变量名: 读取头部和参数的时候,判断是否有替换的值。有就替换。
- 2、从环境变量获取属性值-- 变量值
- 3、将变量名替换为属性值,再去用于发送接口请求【头部也有,参数也有】
- 难点:代码如何自动识别这些要替换的数据?–用占位符 ; 占位符包裹的变量名出来? — 正则表达式匹配
"""
思路如下:
1、先从excel标题读取【提取响应结果数据】这列; -- 读取出来是字符串
2、转化为字典-- json.loads()反序列化
3、分别渠道这个提取表达式里的 key 和value ,key是字段名,value是jsonpath表达式。 -- 用for循环遍历
4、从接口的响应结果里使用这个jsonpath表达式提取数据。
"""
# 从excel里读取出来的 提取形影结果的表达式
import json
from jsonpath import jsonpath
# 这个类就是为了存储环境变量 实现环境变量的共享的
class EnviData:
pass
response = {"access_token":"0efdce50-0e2f-4ed0-b4d1-944be5ab518a",
"token_type":"bearer","refresh_token":"4bfc3638-e7e4-4844-a83d-c0f8340bc146",
"expires_in":1295999,"pic":"http://mall.lemonban.com:8108/2023/09/b5a479b28d514aa59dfa55422b23a6f0.jpg",
"userId":"46189bfd628e4a738f639017f1d9225d","nickName":"lemon_auto","enabled":True}
extract_data = '{"access_token":"$..access_token","token_type":"$..token_type"}'
# 第一步: 反序列化 -字典
extract_data = json.loads(extract_data)
for k,v in extract_data.items(): # k 是access_token 变量名字,v是$..access_token
# 使用jsonpath表达式 提取login响应结果里的值
value = jsonpath(response,v)[0] # 是access_token的具体值
print(value)
# 存起来到环境变量里去
setattr(EnviData,k,value)
print(EnviData.__dict__)
运行结果如下:
0efdce50-0e2f-4ed0-b4d1-944be5ab518a
bearer
{'__module__': '__main__', '__dict__': <attribute '__dict__' of 'EnviData' objects>, '__weakref__': <attribute '__weakref__' of 'EnviData' objects>, '__doc__': None, 'access_token': '0efdce50-0e2f-4ed0-b4d1-944be5ab518a', 'token_type': 'bearer'}
正则表达式提取
- 正则表达式是一组由字母和符号组成的特殊文本, 它可以用来从文本中找出满足你想要的格式的句子.
- 主要针对字符串:
- 1、判断字符串是否匹配我的规则
- 2、提供正则表达式,从字符串提取出来匹配的内容。
- 正则非常强大,开发特别是前端开发用的很多,语法非常多,没有必要都掌握,用到了再去查就可以。目前先学习我们要用的即可。
re相关的文章: https://www.cnblogs.com/Simple-Small/p/9150947.html
regular表达式学习手册: https://tool.oschina.net/uploads/apidocs/jquery/regexp.html(规则)
https://gitee.com/thinkyoung/learn_regex
在线的正则验证:https://c.runoob.com/front-end/854/
-
正则的语法规则:
- 1、匹配一个字符 (元字符)
- \d 只匹配数字0-9
- \w 匹配[0-9A-Za-z_] 数字字母下划线 支持中文
- . 除了换行符(\n)以外的所有字符。
- 2、正则的匹配次数
- *匹配前一个字符,0次或者多次 ==任意次
- +匹配前一个字符,1次或者多次
- ?匹配前一个字符,0次或者1次
- 1、匹配一个字符 (元字符)
-
我们用的做多就是万能表达式: 非贪婪匹配的 .*?
- ? 非贪婪匹配模式:找到了## 两个后 不会再找了就匹配成功了;
- 贪婪模式:尽可能去进行更多的匹配
- 非贪婪模式:尽可能少的去进行匹配
- ? 非贪婪匹配模式:找到了## 两个后 不会再找了就匹配成功了;
-
Python的正则的库,内置库 re , 直接导入re 库,免安装
- re模块函数 : findall(),search()
- findall(正则表达式, 目标字符串) ,字符串从头到尾去匹配,只要符合要求就会拿出来
- 多个符合的结果 存到列表里,返回一个列表。
- 如果只想获取里面的变量名 不需要这个## 限定符,可以加个括号括起来: 加了括号,就是只提取括号里的内容,就是左右边界了。
- search(正则表达式, 目标字符串)方法
- 搜索查找,如果匹配的正则符合的话,返回Match类型的对象,不符合就返回None
- Match类型的对象(search函数的返回值)的方法group() - 正则分组,可以返回的匹配的整体子串-整体占位
符内容 #prodId# - search函数找到了第一个符合的之后,它就不会再继续往后去查找
- findall(正则表达式, 目标字符串) ,字符串从头到尾去匹配,只要符合要求就会拿出来
- re模块函数 : findall(),search()
"""
*
re模块函数 : findall(),search()
* findall(正则表达式, 目标字符串) ,字符串从头到尾去匹配,只要符合要求就会拿出来
* 多个符合的结果 存到列表里,返回一个列表。
* 如果只想获取里面的变量名 不需要这个## 限定符,可以加个括号括起来: 加了括号,就是只提取括号里的内容,## 就是左右边界了。
* search(正则表达式, 目标字符串)方法
* 搜索查找,如果匹配的正则符合的话,返回Match类型的对象,不符合就返回None
* Match类型的对象(search函数的返回值)的方法group() - 正则分组,可以返回的匹配的整体子串-整体占位符内容 #prodId#
* search函数找到了第一个符合的之后,它就不会再继续往后去查找
- 循环找: 先找到第一个,替换掉这个; 继续第二次,替换掉,第三次。。。。
- 确定循环次数么?--不能确认循环次数的时候,用while。
- while循环先进去,找不到占位符 -- 返回值None ,跳出循环。
思路:
1、环境变量里已经有了 属性名 --属性值
class EnviData:
prodId = 7717
skuId = 4563
2、先从类里获取属性值 -- getattr(EnviData,prodId) === prodId这个名字
3、先用正则找到这个字符串里的 占位符标记的字符串 ,replace(标记符,类属性值)
注意: 做的事情
- 第一个: 取到 #prodId# == 被替换掉的子字符串
- 第二个: prodId == 获取类属性变量。
"""
import re
str_data = '{"basketId": 0, "count": 1, "prodId": #prodId#, "shopId": 1, "skuId": #skuId#}'
# findall(正则表达式, 目标字符串) ,字符串从头到尾去匹配,只要符合要求就会拿出来
result = re.findall("#.*?#",str_data) # 结果是 #prodId#
print(result)
# 得到## 限定符里面的变量名:--加上括号就只会得到 #里面的值#
var_name = re.findall("#(.*?)#",str_data) #结果 prodId
print(var_name)
# search(正则表达式, 目标字符串)方法
result = re.search("#(.*?)#",str_data) # 结果是一个对象
print(result)
# 对象的group()方法 - 正则分组,可以返回的匹配的整体子串
value = result.group() # 结果是 #prodId#
print(value)
var_name = result.group(1) # 第一个分组的值 结果是 prodId
print(var_name)
# 没有匹配到结果--None
result = re.search("#.*?!#",str_data) # 结果是一个对象
print(result)
运行结果如下:
['#prodId#', '#skuId#']
['prodId', 'skuId']
<re.Match object; span=(38, 46), match='#prodId#'>
#prodId#
prodId
None
循环正则提取
"""
*
re模块函数 : findall(),search()
* findall(正则表达式, 目标字符串) ,字符串从头到尾去匹配,只要符合要求就会拿出来
* 多个符合的结果 存到列表里,返回一个列表。
* 如果只想获取里面的变量名 不需要这个## 限定符,可以加个括号括起来: 加了括号,就是只提取括号里的内容,## 就是左右边界了。
* search(正则表达式, 目标字符串)方法
* 搜索查找,如果匹配的正则符合的话,返回Match类型的对象,不符合就返回None
* Match类型的对象(search函数的返回值)的方法group() - 正则分组,可以返回的匹配的整体子串-整体占位符内容 #prodId#
* search函数找到了第一个符合的之后,它就不会再继续往后去查找
- 循环找: 先找到第一个,替换掉这个; 继续第二次,替换掉,第三次。。。。
- 确定循环次数么?--不能确认循环次数的时候,用while。
- while循环先进去,找不到占位符 -- 返回值None ,跳出循环。
思路:
1、环境变量里已经有了 属性名 --属性值
class EnviData:
prodId = 7717
skuId = 4563
2、先从类里获取属性值 -- getattr(EnviData,prodId) === prodId这个名字
3、先用正则找到这个字符串里的 占位符标记的字符串 ,replace(标记符,类属性值)
注意: 做的事情
- 第一个: 取到 #prodId# == 被替换掉的子字符串
- 第二个: prodId == 获取类属性变量。
"""
import re
str_data = '{"basketId": 0, "count": 1, "prodId": #prodId#, "shopId": 1, "skuId": #skuId#}'
class EnviData:
prodId = 7717
skuId = 4563
while True:
# search(正则表达式, 目标字符串)方法
result = re.search("#(.*?)#",str_data) # 结果是一个对象
if result is None: # 如果没有占位符 就是None 跳出循环
break
mark = result.group() # 结果是 #prodId# --要被替换的子字符串
var_name = result.group(1) # 第一个分组的值 结果是 prodId
# 从环境变量里获取变量名对应的属性值 prodId-- 属性值 7717,替换的数据
var_value = getattr(EnviData,var_name) # 结果 : 7717--int类型
# 目标字符串的替换,赋值给一个变量
# 注意:replace方法要求 两个参数都应该是字符串。
str_data = str_data.replace(mark,str(var_value))
print(str_data)
运行结果如下:
{"basketId": 0, "count": 1, "prodId": 7717, "shopId": 1, "skuId": #skuId#}
{"basketId": 0, "count": 1, "prodId": 7717, "shopId": 1, "skuId": 4563}
多接口动态参数封装
提取响应结果的函数 – 是在接口执行完成后 得到响应结果
都需要在requests_api函数里扩展 ==对应的位置调用这两个函数就可以了。
提取响应结果的函数 – 是在接口执行完成后 得到响应结果
handle_replace.py 内容如下:
"""
1、def封装
2、参数化
3、返回值: 最终要拿到替换后的字符串 --- 头部 参数 要用于发送接口测试的
4、加上日志: 但凡你想确认数据结果的地方 都可以加上日志
5、因为有些接口不需要做数据提取,所以判空处理:
6、异常捕获: 因为有可能环境变量里没有这个属性名 和属性值
"""
import re
from loguru import logger
from tools.envi_data import EnviData
def replace_mark(str_data):
while True:
if str_data is None:
return
result = re.search("#(.*?)#",str_data)
if result is None: # 如果没有占位符 就是None 跳出循环
break
mark = result.group() # 结果是 #prodId# --要被替换的子字符串
logger.info(f"要被替换的子字符串:{mark}")
var_name = result.group(1) # 第一个分组的值 结果是 prodId
logger.info(f"要提取环境变量的属性名:{var_name}")
try:
var_value = getattr(EnviData,var_name) # 结果 : 7717--int类型
except AttributeError as e:
logger.error(f"环境变量里不存在这个属性:{var_name}")
raise e
logger.info(f"要提取环境变量的属性值:{var_value}")
str_data = str_data.replace(mark,str(var_value))
logger.info(f"替换完成后的字符串是:{str_data}")
return str_data
if __name__ == '__main__':
str_data = '{"basketId": 0, "count": 1, "prodId": #prodId#, "shopId": 1, "skuId": #skuId#}'
replace_mark(str_data)
替换头部和参数里的占位符的函数 – 是在发送接口请求之前
envi_data.py内容如下:
# 这个类就是为了存储环境变量 实现环境变量的共享的
class EnviData:
pass
handle_extract.py 内容(获取需要提取的字段名)如下:
"""
1、def封装
2、参数化
3、返回值: 因为数据都存在环境变量 所以不需要返回值
4、加上日志: 但凡你想确认数据结果的地方 都可以加上日志
5、因为有些接口不需要做数据提取,所以判空处理:
"""
import json
from jsonpath import jsonpath
from loguru import logger
from tools.envi_data import EnviData
def extract_response(response,extract_data):
# 因为有些接口不需要做数据提取,所以判空处理:
if extract_data is None:
logger.info("这条用例不需要做响应结果的数据提取!")
return
# 第一步: 反序列化 -字典
logger.info("-----------------响应结果提取开始------------------------------")
extract_data = json.loads(extract_data)
logger.info(f"提取的向红结果的表达式是:{extract_data}")
for k,v in extract_data.items(): # k 是access_token 变量名字,v是$..access_token
# 使用jsonpath表达式 提取login响应结果里的值
value = jsonpath(response.json(),v)[0] # 是access_token的具体值
# 存起来到环境变量里去
setattr(EnviData,k,value)
logger.info(f"提取并设置环境变量之后的类属性是:{EnviData.__dict__}")
if __name__ == '__main__':
response = {"access_token": "0efdce50-0e2f-4ed0-b4d1-944be5ab518a",
"token_type": "bearer", "refresh_token": "4bfc3638-e7e4-4844-a83d-c0f8340bc146",
"expires_in": 1295999,
"pic": "http://mall.lemonban.com:8108/2023/09/b5a479b28d514aa59dfa55422b23a6f0.jpg",
"userId": "46189bfd628e4a738f639017f1d9225d", "nickName": "lemon_auto", "enabled": True}
extract_data = '{"access_token":"$..access_token","token_type":"$..token_type"}'
extract_response(response,extract_data)
handle_requests.py文件如下:
"""
方法优化:
1、日志加上
2、测试用例方法里调用夹具 获取返回值。
- 更新requests-api,需要做token处理:
- 设置一个默认参数:token = None
- 如果接口需要鉴权,测试用例里调用夹具,得到token,requests_api传递token参数;--requests 更新头部
- 如果接口不需要鉴权: token不传 None。 不会做更新头部的操作。
"""
import json
import requests
from tools.handle_path import pic_path
from loguru import logger
from tools.handle_extract import extract_response
from tools.handle_replace import replace_mark
def requests_api(casedata,token=None):
method = casedata["请求方法"]
url = casedata["接口地址"]
headers = casedata["请求头"]
params = casedata["请求参数"]
# 在发送请求之前完成头部和参数的替换--调用替换的函数==结果是字符串
headers = replace_mark(headers)
params = replace_mark(params)
# 反序列操作: 结合判空处理,
if headers is not None:
headers = json.loads(headers)
if token is not None: # 这是做接口如果需要鉴权,传进来token 更新头部信息。
headers["Authorization"] = token # 字典新增 / 修改
if params is not None:
params = json.loads(params)
logger.info("---------------------------请求消息-----------------------------------")
logger.info(f"请求方法是{method}")
logger.info(f"请求地址是{url}")
logger.info(f"请求头部是{headers}")
logger.info(f"请求参数是{params}")
#接口请求可能是get post put等各种请求方法 分支判断
if method.lower() == "get":
resp = requests.request(method=method, url=url, params=params,headers=headers)
elif method.lower() == "post":
if headers is None:
logger.info("头部为空,检查excel表格里头部信息!")
return
# post请求:content-type的类型有关系。需要对每一种类型做处理 分支判断
if headers["Content-Type"] == "application/json":
resp = requests.request(method=method, url=url, json=params, headers=headers)
if headers["Content-Type"] == "application/x-www-form-urlencoded":
resp = requests.request(method=method, url=url, data=params, headers=headers)
if headers["Content-Type"] == "multipart/form-data":
# 发送请求的时候不能带上 'Content-Type': 'multipart/form-data' 删除之后才发送接口请求。
headers.pop("Content-Type") # 字典删除元素
filename = params["filename"] # 文件名字 值
file_obj = {"file": (filename, open(pic_path/filename, "rb"))} # 文件参数
logger.info(f"文件接口的参数是:{file_obj}")
logger.info(f"文件接口的头部是:{headers}")
resp = requests.request(method=method, url=url,headers=headers,files=file_obj)
elif method.lower() == "put":
pass
logger.info("------------------------------响应消息-----------------------------")
logger.info(f"接口响应状态码是:{resp.status_code}")
logger.info(f"接口响应体是:{resp.text}")
# 提取响应结果的数据-- 调用提取数据的函数
extract_response(resp,casedata["提取响应字段"])
return resp