HttpRunner的PB序列化工具类解决方案(python3)
背景
年初的时候团队内落地了HttpRunner3框架,简单介绍下:HttpRunner 是一款由python开发的面向 HTTP(S) 协议的开源通用测试框架,用例脚本为 YAML/JSON 格式,3.0版本支持py格式。
HttpRunner 依赖开源库requests ,pytest ,pydantic ,allure 和 locust,可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。
PB指的是Protocol Buffers 又简称为 Protobuf,是 Google 推出的一种二进制数据交换格式(类似json、xml一样,但更轻量)。
Protobuf 有自己的编译器,在 Linux 中叫做 protoc
,可以解释 .proto
文件并且声称对应语言的源文件,目前已支持多种语言,githab链接,如何接入PB网上很多教程,这里就不展开说了。
问题
目前公司前后端接口已经接入了PB,老接口暂时json入参不变,新接口采用PB格式,但HttpRunner不支持PB格式入参,新接口测试就无法进行。
解决方案
如果是第一次肯定一头雾水,于是我收集信息:
- pb是谷歌序列化协议,可以简单理解为base64编码解码,不同的是解析规则是我们自己定义的,而规则就是.proto文件(其实说.porto文件是接口文档更合适)。
- 一个proto文件就是一个解析规则,而文件中的一个message就是结构化数据,对应一个接口请求的结构或者出参的结构,也可以抽象理解为定义一个类,enum就是枚举,以数字编号作为主键等等。
- proto文件跟语言无关,其他语言要解析,需要对应语言的编译器工具把proto文件编译成目标语言,python就会编译成类似xxx_pb2.py的文件。
- 使用范例可以自行安装proto(pip install protobuf,验证用 import google.protobuf 不报错就ok),调用方法实例,json格式的方法如下
google.protobuf.json_format
包含用于以 JSON 格式打印协议消息的例程。
简单使用示例:
# 创建一个proto对象并将其序列化为json格式字符串。
message_object= my_proto_pb2.MyMessage(foo='bar')
json_string = json_format.MessageToJson(message_object)
# 解析一个json格式的字符串到proto对象。
message = json_format.Parse(json_string, my_proto_pb2.MyMessage()) --my_proto_pb2是指编译后pb文件,MyMessage()这是对应的message方法名
结论如下:
- 前后端开发编写接口文档,也就是.proto文件(这里跟前后端语言无关,使用PB格式的语法),一个项目可以根据服务划分,每个服务可能包含多个.proto文件,每个.proto文件也可能对应多个接口。
- 前后端采用官方编译器编译每个.proto文件,生成自己使用的语言类包(比如python就是xxx_pb2.py)。
- 有了xxx_pb2.py文件,就引用google.protobuf.json_format包,调用相应的方法,针对特定的message,就可以把json的字符串解析为PB的二进制格式了。
于是我的解决思路:一,传入接口入参调用pb;二,根据入参找到对应的接口pb2文件;三,解析该接口入参数据;四,返回替换请求的入参;
第一步
- HttpRunner的接口请求前都有前置处理,所以只需要在debugtalk文件中写一个把json序列化为PB的共用方法就行。
- 为了使debugtalk简洁点,处理PB序列化的可以单写一个转换类,上面的方法引用这个转换类。
- 这个转换类至少需要提供json解析为PB、PB序列化为json的两个方法。
- 可能需要其他一些日志、过滤、加密方法(根据实际情况来)。
第二步
问题1:前面说到不用关心.proto文件,那xxx_pb2.py怎么来?
好在我们前端人员做了转换工程,每次更新项目的.proto文件,都会上传到gitlab上,可根据自己的语言拉取即可。
问题2:现在_pb2文件有了,但是.proto肯定存在多个接口(对应多个message),而HttpRunner中每个接口的请求入参都需要解析,如何根据接口名找到对应的message呢?
还是前端做了一个json文件(index.json),里面存在接口与其关联的message信息,于是查找这个文件即可,文件内容样式如下。
于是:
- 转换类需要导入_pb2.py文件、json文件,由于不是同一个项目,需要使用到git的子模块submodule功能。
- 转换类需要实现一个查找方法,输入接口的请求url在json文件中找到对应的接口message。
第三步
这一步为重点,需要实现json解析为PB、PB序列化为json的两个方法
- 根据上面调用实例,两个方法都需要json字符串、pb2文件名、message名三个入参。
- 由于每个接口有对应pb2文件,所以这个也是变量,可以用接口的路径拼接起来。
- 两个方法应该返回对应的字符串,注意PB是字节流bytes格式。
第四步
基本上第三步已经完成了转换类,这一步主要是针对debugtalk的方法
- debugtalk中json序列化为PB的共用方法回填数据时,需要选择from_data格式(post的from_data格式才支持字节流bytes格式)。
- 如果涉及接口签名,可根据实际情况添加方法。
代码实现
上面的思路也是在写的过程慢慢思考的,目前按照这个实现项目已经持续运行了一段时间,主要是第二步内部转换工程已经实现了,倒省了不少事。
下面为实现代码,可能有瑕疵,欢迎各同学指正。
debugtalk调用方法
其中request为HttpRunner内置的请求对象,可对请求进行前置处理
def json_proto(request):
"""
序列化request的入参json
:param request: 接口请求对象
:return: 序列化后的接口请求对象
"""
if request["method"] == "POST":
if 'data' in request and request["data"]:
origin_json = json.dumps(request["data"]).replace("'", "\"")
print("INF: 原始json为", origin_json)
request["data"] = ProtoDataFormat().get_proto_data(request["url"], origin_json, "request")
print("INF: 最后json为", request["data"])
if request['method'] == 'GET':
if 'params' in request and request["params"]:
origin_json = json.dumps(request["params"]).replace("'", "\"")
print("INF: 原始json为", origin_json)
params = ProtoDataFormat().get_proto_data(request["url"], origin_json, "request")
request['params'] = "bbValue=" + params
print("INF: 最后json为", request["params"])
return request
转换类
# coding: utf-8
import base64
import importlib
import google.protobuf.json_format as json_format
import json
import re
import sys sys.path.append("./subModuleForPB/b-python") class ProtoDataFormat:
def __init__(self):
self.pwd = r"./subModuleForPB/b-js/mock/index.json"
try:
self.read_json(self.pwd)
except:
print("当前目录无index.json,尝试更改pwd路径属性") def get_proto_data(self, url, paramer, type):
"""
主要实现功能是 json格式数据转成pb格式
:param url: 传入的接口Url,去掉域名IP地址
:param paramer: 传入的json格式的原始字符串值
:return: json数据转成pb协议格式并Base64的数据
"""
print("INF: json字符串开始预处理", paramer)
if type =="request":
type = "requestMessage"
elif type =="response":
type = "responseMessage"
else:
return "type不合法"
# 兼容url多余的/路径符
# if url[0] =="/":
# url = url[1:]
# 查找对应url的message正则
pattern1 = ".*{(.*?), \"url\": \"" + url + "\""
# 读取index.json文件,转为json字符串
load_str = self.read_json(self.pwd)
# print("INF: json字符串开始预处理2")
# 提取json中的message信息
relt = self.re_str(pattern1, load_str)
if relt:
proto_message = relt[type]
else:
print("Error: 提取json中的message信息失败 ", relt)
# 查找pb2文件的path正则
pattern2 = ".*{(.*?), \"name\": \"" + proto_message + "\""
# 提取json中的pb2文件的path信息
path = self.re_str(pattern2, load_str)
if path:
# 修改pb2文件的path为导入模块格式
mod_path = self.path_module(path["path"])
else:
print("Error: 提取json中的pb2文件的path信息失败 ", path)
print("INF: json字符串预处理完成")
return self.json_proto_base64(paramer, mod_path, proto_message[17:]) def get_json_data(self, url, paramer, type):
"""
主要实现功能是 json格式数据转成pb格式
:param url: 传入的接口Url,去掉域名IP地址
:param paramer: 传入的pb格式的原始字符串值
:return: pb协议格式数据转成json的数据
"""
if type =="request":
type = "requestMessage"
elif type =="response":
type = "responseMessage"
else:
return "type不合法"
# 查找对应url的message正则
pattern1 = ".*{(.*?), \"url\": \"" + url + "\""
# 读取index.json文件,转为json字符串
load_str = self.read_json(self.pwd)
# 提取json中的message信息
relt = self.re_str(pattern1, load_str)
if relt:
proto_message = relt[type]
else:
print("Error: 提取json中的message信息失败 ", relt)
# 查找pb2文件的path正则
pattern2 = ".*{(.*?), \"name\": \"" + proto_message + "\""
# 提取json中的pb2文件的path信息
path = self.re_str(pattern2, load_str)
if path:
# 修改pb2文件的path为导入模块格式
mod_path = self.path_module(path["path"])
else:
print("Error: 提取json中的pb2文件的path信息失败 ", path)
paramer = base64.b64decode(paramer) return self.proto_json(paramer, mod_path, proto_message[17:]) def proto_json(self, orginjson, path, message):
"""
主要实现功能是 pb格式数据转成json格式
:param orginjson: 传入的json格式的原始字符串值
:param path: 需要导入的model路径,特指xxx_py2文件
:param message: message,特指xxx_py2文件中对应的接口message
:return: pb协议格式转成的json数据
如:message = my_proto_pb2.MyMessage(foo='bar') json_string = json_format.MessageToJson(message)
"""
try:
foo = importlib.import_module(path)
except:
raise ModuleNotFoundError("error: 模块导入失败,尝试修改源文件sys.path的b-python文件夹路径")
fun = eval("foo." + message)
mes = fun() mes.ParseFromString(orginjson)
return json_format.MessageToJson(mes) def json_proto_base64(self, orginjson, path, message):
"""
主要实现功能是 json格式数据转成pb格式
:param orginjson: 传入的json格式的原始字符串值
:param path: 需要导入的model路径,特指xxx_py2文件
:return: json数据转成pb协议格式并Base64的数据
"""
try:
foo = importlib.import_module(path)
except:
print("Error: 模块导入失败,尝试修改源文件sys.path的b-python文件夹路径")
fun = eval("foo."+ message)
print("INF: json字符串开始转换PB")
try:
mes = json_format.Parse(orginjson, fun())
buffer = mes.SerializeToString()
except:
raise Exception("Error: json格式化失败,请检查入参格式")
else:
print("INF: json_proto_base64主函数执行通过")
return base64.b64encode(buffer).decode(encoding = "utf-8") def read_json(self, pwd):
"""
读取index.json文件,并返回对应json字符串
:param pwd: 传入的index.json文件路径
:return: index.json内容json字符串的全部数据
"""
with open(pwd, "r") as load_f:
load_dict = json.load(load_f)
return json.dumps(load_dict) def re_str(self, pattern, str):
"""
json字符串根据正则取出接口对应的message信息
:param pattern: 传入的正则表达式
:param str: 需要查找的原始字符串
:return: 接口对应的message字典,如{"name": "DecrVirtual","requestMessage": "billion.protobuf.BDecrVirtualRequest","responseMessage": ".google.protobuf.Empty","method": "POST"}
"""
search_obj = re.search(pattern, str)
lis = []
if search_obj:
for i in search_obj.groups():
i = "{"+ i +"}"
lis.append(i)
if lis:
dic = json.loads(lis[0])
return dic
else:
return {} def path_module(self, path):
"""
修改path为模块路径,替换"\"为".",加上"_pb2"
:param path: index.json中接口文件路径
:return: 可以导入的xxx_pb2文件的module路径
"""
path = str(path).strip("/")
new_path = path.replace("/",".")
new_path = new_path[:-5]
return new_path + "_pb2" if __name__ == '__main__':
"""
调用 get_json_data() 方法完成 pb协议的数据转成json格式
调用 get_proto_data() 方法完成 json格式的数据转换成pb协议的数据
"""
orginjson = '{"pageInfo": {"pageNo": 1, "pageSize": 10}, "topicInfo": {}, "searchType": "B_SEARCH_TYPE_NEW"}'
apiUrl = "/fleaTopic/topic/v1/releaseTopic"
# pbvalue = "Ci0QATABUicKFBIM57u/6Imy6aOf5ZOBIAEwAzgDEgcoATDuBUAKIgYKBG51bGw="
# jp.pwd = r"D:\b-js\mock\index.json"
type = "request"
bbvalue = ProtoDataFormat().get_proto_data(apiUrl, orginjson, type)
print(bbvalue)
# bbjson = ProtoDataFormat().get_json_data(apiUrl, pbvalue, type)
# print(bbjson)
HttpRunner的PB序列化工具类解决方案(python3)的更多相关文章
- Java 序列化工具类
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sun.misc.BASE64Decoder; import sun.m ...
- 序列化工具类({对实体Bean进行序列化操作.},{将字节数组反序列化为实体Bean.})
package com.dsj.gdbd.utils.serialize; import java.io.ByteArrayInputStream; import java.io.ByteArrayO ...
- JSON序列化必看以及序列化工具类
1.要序列化的类必须用 [DataContract] 特性标识 2.需要序列化的属性应用 [DataMember] 特性标识,没有该特性则表示不序列化该属性.类亦如此! 3.可以网络上找封装好 ...
- Protostuff序列化工具类
源代码 package org.wit.ff.util; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStre ...
- redis缓存工具类,提供序列化接口
1.序列化工具类 package com.qicheshetuan.backend.util; import java.io.ByteArrayInputStream; import java.io. ...
- Java 序列化对象工具类
SerializationUtils.java package javax.utils; import java.io.ByteArrayInputStream; import java.io.Byt ...
- Android开发常用工具类
来源于http://www.open-open.com/lib/view/open1416535785398.html 主要介绍总结的Android开发中常用的工具类,大部分同样适用于Java. 目前 ...
- 最全Android开发常用工具类
主要介绍总结的Android开发中常用的工具类,大部分同样适用于Java. 目前包括 HttpUtils.DownloadManagerPro.Safe.ijiami.ShellUtils.Pack ...
- java工具类
1.HttpUtilsHttp网络工具类,主要包括httpGet.httpPost以及http参数相关方法,以httpGet为例:static HttpResponse httpGet(HttpReq ...
随机推荐
- 垃圾收集器G1和CMS ,以及老年代和新生代的比例设置
首先 1.G1是包括年轻代和年老代的GC 2.CMS是年老代GC 3.二者在某些时候都需要FullGC(serial old GC)的辅助 ###CMS收集器:CMS(ConCurrent Mark ...
- Math.round(11.5)等於多少? Math.round(-11.5)等於多少?
Math类中提供了三个与取整有关的方法:ceil.floor.round,这些方法的作用与它们的英文名称的含义相对应,例如,ceil的英文意义是天花板,该方法就表示向上取整,所以,Math.ceil( ...
- 为什么Java中不支持多重继承?
我发现这个 Java 核心问题很难回答,因为你的答案可能不会让面试官满意,在大多数情况下,面试官正在寻找答案中的关键点,如果你提到这些关键点,面试官会很高兴.在 Java 中回答这种棘手问题的关键是准 ...
- Eureka和Zookeeper区别?
(1)Eureka取CAP的AP,注重可用性,Zookeeper取CAP的CP注重一致性. (2)Zookeeper在选举期间注册服务瘫痪,虽然服务最终会恢复,但选举期间不可用. (3)eureka的 ...
- JavaScript的一些实用操作(逐步添加)
1.js代码简洁高效计时 console.time('a'); //记录时间开始 ... console.timeEnd('a'); //记录时间结束 a: 12857.81103515625ms / ...
- Java 中是如何支持正则表达式操作的?
Java 中的 String 类提供了支持正则表达式操作的方法,包括:matches(). replaceAll().replaceFirst().split().此外,Java 中可以用 Patte ...
- 什么叫线程安全?servlet 是线程安全吗?
线程安全是编程中的术语,指某个函数.函数库在多线程环境中被调用时,能够 正确地处理多个线程之间的共享变量,使程序功能正确完成. Servlet 不是线程安全的,servlet 是单实例多线程的,当多个 ...
- Redis 的内存用完了会发生什么?
如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正 常返回.)或者你可以将 Redis 当缓存来使用配置淘汰机制,当 Redis 达到内存 上限时会冲刷掉旧的内容.
- SpringDataJpa 实体类过滤伪删除
当需要过滤实体类的数据时,根据伪删除字段进行过滤,需要使用Hibernate提供的@Where注解 使用方式: @Entity(name = "Account") @Where( ...
- Mybatis useGeneratedKeys无法返回主键解决
1.项目环境--SpringBoot下的SSM+Maven 2.问题出现位置--Dao层和Mapper文件 错误代码如下图: dao层: mapper文件: 错误代码分析: 使用useGenerate ...