对于后端的接口文档,作为测试的各位看官应该都不陌生,而且大家平时没少被以下两种情况困扰吧:
1.后端开发突然说,这次修改了某个功能的几个接口,前端同学对应修改下,然后测试同学测一测就上线。此时前端和测试同学想必都是一脸懵逼,后端开发这是又改了哪几个接口啊?都改了啥呀?直接去接口文档上看,啥也看不出来,还是只有去一个个的找后端开发确认,效率低下不说还很容易遗漏。
2.大家在做接口自动化的时候,需要不断的去接口文档中一个个的复制 api 和对应请求参数,还得不断切换到接口文档对应页面查看参数定义,重复工作量极大,效率极低。
针对以上两个长期困扰大家的痛点,于是催生了 swagger 与天眼平台的结合。下面部分具体介绍了天眼平台是如何解决这两个痛点的,请各位看官收藏后再仔细阅读。如有不足或建议,请积极留言。ღ( ´・ᴗ・· )比心。
对于大部分开发都在使用的 swagger 各位看官应该不陌生吧,简单来说就是一个生成接口文档的第三方类库。开发在代码中引入 swagger 后即可自动生成可视化 RESTful 风格的 Web 服务用于描述和调用接口。
效果的话,就是下面这样。但是和大部分接口文档一样,它缺少版本管理和版本 diff 功能,后端开发改了某个接口,在接口文档上体现不出来。
1.将 swagger 接口文档,导入天眼平台数据库
实现接口文档 diff 的话第一步是解析 swagger 文档将其落库,这是我们天眼平台的接口文档表设计,需要将 swagger 文档解析成对应字段落库。我们支持两种导入方式 json 文件导入和 URL 导入。
这里强调下网关前缀,因为大部分公司都有网关层,会自动给业务端代码加上类似/api 这种前缀。所以当我们直接用导入文档中的 api 去做自动化时会请求不通,所以增加了一个填写网关前缀的功能,落库时能将接口文档的 api 和网关前缀进行拼接。
URL 导入是指用户直接输入 swagger 文档的 URL,我们通过字符串截取获取 ip,然后请求 ip+/swagger-resources 得到下面的结果,然后再基于这个返回结果中的 url 拼接 ip,直接访问即可获取到 json 格式的 swagger 文档的文本内容了。
获取到 json 文本内容后,那么我们先看看 swagger json 文件的格式(本次解析针对 swagger3 以上,2 的话结构类似)
解析 JSON 用的是 jackson。主要写一些遇到较多坑的地方吧,每个项目开发人员使用 swagger 的习惯不太一样,这样的话会导致可能你写的解析方法不能覆盖所有的场景,这个也只能根据具体遇到的情况去不断完善了。核心主要解析请求内容,主要是 parameters 和 requestBody 两种,下面的话附上该部分的主要代码~
private static ObjectMapper objectMapper = new ObjectMapper();
public static <T> T readValue(String jsonStr, Class<T> clazz) throws IOException {
return objectMapper.readValue(jsonStr, clazz);
}
Map<String, Object> map = JsonUtils.readValue(jsonStr, HashMap.class);
@Data
public class RequestVo implements Serializable {
/**
* 参数名
*/
private String name;
/**
* 请求类型path/query/body
*/
private String type;
/**
* 参数类型
*/
private String paramType;
/**
* 是否必填
*/
private Boolean required;
/**
* 说明
*/
private String description;
/**
* style
*/
private String style;
/**
*
*/
private List<RequestVo> properties;
}
//处理请求参数为parameters
private List<RequestVo> processRequestList(List<LinkedHashMap> parameters, Map<String, ModelAttrVo> componentsMap) {
List<RequestVo> requestList = new ArrayList<>();
if (!CollectionUtils.isEmpty(parameters)) {
for (Map<String, Object> param : parameters) {
RequestVo request = new RequestVo();
request.setName(String.valueOf(param.get("name")));
request.setType(String.valueOf(param.get("in")));
request.setRequired((Boolean) param.get("required"));
request.setStyle(String.valueOf(param.get("style")));
try {
Map<String, Object> schema = (Map) param.get("schema");
String paramType = String.valueOf(schema.get("type"));
if (!StringUtils.isEmpty(schema.get("format"))) {
paramType = paramType + "(" + String.valueOf(schema.get("format")) + ")";
}
if (!StringUtils.isEmpty(schema.get("$ref"))) {
String modelName = String.valueOf(schema.get("$ref"));
ModelAttrVo modelAttrVo = componentsMap.get(modelName);
paramType = modelAttrVo.getType();
request.setDescription(modelAttrVo.getDescription());
}
if(!StringUtils.isEmpty(schema.get("enum"))){
String enumStr = String.valueOf(schema.get("enum"));
if (!StringUtils.isEmpty(param.get("description"))){
enumStr= String.valueOf(param.get("description"))+"\t"+enumStr;
}
request.setDescription(enumStr);
}else{
request.setDescription(String.valueOf(param.get("description")));
}
request.setParamType(paramType);
}catch (Exception e){
}
requestList.add(request);
}
}
return requestList;
}
//处理请求为application/json
private List<RequestVo> processRequestList(Map<String, ModelAttrVo> componentsMap, String ref) {
List<RequestVo> requestList = new ArrayList<>();
ModelAttrVo modelAttr = componentsMap.get(ref);
//获取properties大小,
int pSize = 0;
if (!StringUtils.isEmpty(modelAttr.getProperties())) {
pSize = modelAttr.getProperties().size();
}
for (int i = 0; i < pSize; i++) {
requestList = processModelProperties(modelAttr.getProperties(), componentsMap);
}
return requestList;
}
private List<RequestVo> processModelProperties(List<ModelAttrVo> properties, Map<String, ModelAttrVo> componentsMap) {
List<RequestVo> requestList = new ArrayList<>();
if (!CollectionUtils.isEmpty(properties)) {
for (ModelAttrVo param : properties) {
RequestVo request = new RequestVo();
request.setName(param.getParmName());
request.setDescription(param.getDescription());
if (!StringUtils.isEmpty(param.getProperties())) {
request.setType("body");
request.setParamType("");
request.setProperties(processModelProperties(param.getProperties(), componentsMap));
} else if (StringUtils.isEmpty(param.getProperties())) {
String name = param.getParmName().substring(0, 1).toUpperCase() + param.getParmName().substring(1);
if (!StringUtils.isEmpty(componentsMap.get("#/components/schemas/" + name))) {
ModelAttrVo modelAttrVo = componentsMap.get("#/components/schemas/" + name);
request.setRequired(modelAttrVo.getRequired());
request.setParamType(modelAttrVo.getType());
request.setDescription(modelAttrVo.getDescription());
} else {
request.setRequired(param.getRequired());
request.setParamType(param.getParmType());
}
}
requestList.add(request);
}
}
return requestList;
}
json 文件导入是指支持用户直接上传 swagger 文档的离线文档文件,解析方式的话和 url 导入大同小异。
2.接口文档版本控制
第二步是再次导入 swagger 文档的时候判断接口是否有变化。我们是怎么判断接口怎么变化的了,这里主要使用的zjsonpatch,我们会遍历导入接口文档的每一个接口,和数据库中对应信息进行比对,当存在改变时,我们会将当前数据库中的数据写入到一张历史接口文档信息表中,然后最新接口文档落库并打上接口存在变更的标记。当接口文档没有更新时跳过。
//getDiffJSonArray
private JSONArray getDiffJSonArray(String json1, String json2) {
ObjectMapper mapper = new ObjectMapper();
EnumSet<DiffFlags> flags = DiffFlags.dontNormalizeOpIntoMoveAndCopy().clone();
JsonNode patch = null;
try {
patch = JsonDiff.asJson(mapper.readTree(json1), mapper.readTree(json2), flags);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return JSONArray.parseArray(patch.toString());
}
页面效果:
3.版本 diff
第三步也就是接口新老版本的 diff 了,使用上面提到的 zjsonpatch 的方法,能返回 [{"op":"remove","path":"/a","value":0},{"op":"add","path":"/b/2","value":0}] 这样的结果,我们可以知道新旧接口的接口文档中有哪些参数发生了哪些改变。获取到这个之后可以使用 JsonPatch.applyInPlace(JsonNode patch, JsonNode source) 将操作绑定到对应接口文档的参数中。这样前端进行渲染的时候就可以直观的标记新老版本的具体修改点。这里的话需要倒序处理对比的结果,不然会导致前端展示的时候新旧接口参数顺序对不上。其中需要注意的是,op 为 add 时,新相对于旧 value 需设置为 add,旧相对于新的 value 设置为 delete(为了前端渲染时,新老版本能行行对应)。下面的话附上代码及最终呈现的状态。
//获取DiffArrayVo 新相对于旧
private List<DiffArrayVO> processDiffArray(JSONArray jsonArray) {
List<DiffArrayVO> processList = new LinkedList<>();
Map<String, DiffArrayVO> tempMap = new HashMap<>();
for (int i = 0; i < jsonArray.size(); i++) {
DiffArrayVO tempVo = JSON.parseObject(jsonArray.getString(i), DiffArrayVO.class);
DiffArrayVO diffArrayVo = new DiffArrayVO();
switch (tempVo.getOp()) {
case "add":
String path = getPathString(tempVo.getPath());
diffArrayVo.setPath(path);
diffArrayVo.setOp("add");
diffArrayVo.setValue("add");
tempMap.put(path, diffArrayVo);
break;
case "remove":
String path3 = tempVo.getPath();
diffArrayVo.setOp("add");
diffArrayVo.setPath(path3);
diffArrayVo.setValue("XXXXDELETEXXXX");
processList.add(diffArrayVo);
break;
case "replace":
String path2 = getPathString(tempVo.getPath());
if (tempVo.getPath().endsWith("/name")) {
diffArrayVo.setPath(path2);
diffArrayVo.setValue("add");
diffArrayVo.setOp("replace");
tempMap.put(path2, diffArrayVo);
} else {
diffArrayVo.setPath(path2);
diffArrayVo.setValue("update");
diffArrayVo.setOp("replace");
if (!tempMap.containsKey(path2)) {
tempMap.put(path2, diffArrayVo);
}
}
break;
}
}
for (Map.Entry<String, DiffArrayVo> entry : tempMap.entrySet()) {
processList.add(entry.getValue());
}
return processList;
}
//获取替换后的string
private String proceessDiffStr(List<DiffArrayVO> arraySet, String string) {
for (int i = arraySet.size() - 1; i >= 0; i--) {
if (!StringUtils.isEmpty(arraySet.get(i).getOp())) {
String op = "[" + JSONObject.toJSONString(arraySet.get(i)) + "]";
string = parmetersSetOp(op, string);
}
}
return string;
}
//原parameters新增op
private String parmetersSetOp(String op, String parmeters) {
ObjectMapper mapper = new ObjectMapper();
JsonNode a = null;
JsonNode b = null;
try {
a = mapper.readTree(op);
b = mapper.readTree(parmeters);
JsonPatch.applyInPlace(a, b);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return b.toString();
}
当开发的接口文档维护到了天眼平台上,这时和接口自动化的打通就是一件比较 easy 的事情了。
在导入的 swagger 接口文档管理列表,我们提供一个自动化的按钮,点击这个按钮时,我们会根据当前接口文档的所属项目和接口 api,判断这个接口是否已经存在于我们的自动化接口列表中,不存在的话,会带上接口的相关信息直接跳转到新建自动化接口页面,已存在的话直接跳转到对应接口的编辑页面。
当跳转到新建自动化接口详情页面时,前端会根据跳转过来的接口文档的 id,调用对应接口获取该接口的接口文档相关信息,并渲染到页面上。最后的效果如下;
此时在编写自动化接口和用例时,右边即是对应的接口文档,各种接口基本信息默认填充,不再像以前需要不断的去接口文档各种复制粘贴,也摆脱了一个接口几十上百个参数需要手动拼接请求体的噩梦。此时是不是感觉这个功能对于编写接口自动化来说是 yyds。