HttpRunner的PB序列化工具类解决方案(python3)
2022-01-17 20:31 第二个卿老师 阅读(413) 评论(0) 编辑 收藏 举报背景
年初的时候团队内落地了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)