文章目录
- 设计
- 文档工具
- swagger
- RAP2
- 测试和监控
- 外部保护->POSTMAN tests
- 1.添加环境变量
- 2.设置测试方法
- 2.1 全局测试
- 2.2 接口schema校验
- 3.批量运行
- 4.不同环境的处理办法
- 内部保护->prometheus http埋点监控
- 1.`pom.xml` 添加依赖
- 2.配置restTemplate
- 3.micrometer配置
- 4.添加actuator暴露端点
- 5.`prometheus.yml`添加配置
- 6.`grafana`添加dashboard
- 最终效果
- 链接汇总
经常在技术群里或者各种博客中看到大家在争执API测试工具应该用哪个,什么http client,postman,apifox,jmeter,那该如何选择适合自己的工具呢,如何才能提高生产力,更快的做完手上工作呢,其实,归根到底,没有最好,只有适合自己的才是最好的,在接口开发的不同阶段,选择适合自己的工具,来更快的实现目标就行。
CURD多年,前后台测试接口,跨团队测试接口,后台调用外部接口,经常性的会出现各种各样的扯皮现象,主要来来回回就是这几个:
- 接口格式不对
- 接口字段不对
- 接口缺失字段
- 接口不够规范
- 接口不够稳定
那,该如何避免或者节省这之间的来来回回的开销呢?
设计
- 定规范
- 确定请求方式,常用的
GET
POST
,REST规范的GET/POST/DELETE/PUT
,其他的OPTIONS/PATCH/COPY/HEAD/LINK/UNLINK/PURGE/LOCK/UNLOCK/PROPFIND/VIEW
- 定请求参数,是request param还是request body,request body类型是什么
- 接口是否需要安全校验,采用哪种校验方式
- 返回体结构,一般来说后端返回的数据,会基于标准的HTTP STATUS在额外封装一层,用于关联业务或者后台内部的处理逻辑。
{
"code":200,
"msg":"请求成功",
"data":{}
}
- 约定返回体结构的CODE标准,一般采用标准的http status(200/400/401/403/500常用,在实际开发过程中,部分开发人员会对部分http状态码做自定义定义,需在接口最开始,定好全局规范)
- 约定返回体结构的实际业务数据结构,一般来说,包括以下内容:
类型 |
字段名称 |
是否必选 |
类型 |
中文含义 |
文档工具
本人经常用的接口文档工具是swagger和RAP2。但在实际工作中,碰到的部分同事,基于开发人员自己的习惯,总是想着现有代码,才有接口,这就陷入了一个误区。接口不是一个人的事情,往大了说,属于整个项目运转过程中的一个个生命线,往小了说,是接口开发方和接口使用方之间的"冲突博弈"过程。
所以,不管是用什么工具,提前定义好接口字段,然后双方进行各自调试,完成后,进行合并验证,是最高效的开发方式。
swagger
用于团队内部之间的对接开发,所见即所得,可提供实际数据的便捷查询。
RAP2
一般用于跨团队、跨部门的协同开发工作,提供mock,项目组前端同事可直接直接调用测试。
测试和监控
接口对接阶段是最容易发生扯皮的地方,因个人做的是政府相关项目,一般来说,开发环境的数据没法做到百分百模拟正式环境,和其他同事或者团队对接时候,经常性出现这种问题,开发调试阶段,接口不符合规范,好不容易接口调完了,拿到正式环境,数据一上来,发现对方提供的数据标准又有问题。
网络隔离,VPN速度慢,各种来回扯皮,让人烦不胜烦。
于是逐渐开始思考,能否利用自己手上已有的工具,尽量做到尽快定位问题,尽快排查问题呢?经过一段时间的探究,针对接口对接过程中的相关问题,研究出了以下方法论
外部保护->POSTMAN tests
1.添加环境变量
一般来说,环境变量包括IP、参数、token等,即分为两种,静态变量和动态变量,变量一般统一放在collection中的Variables中
使用方法:{{variable_name}}
- 静态变量
静态变量一般就是接口的请求参数,可以直接通过双括号进行引入 - 动态变量
最常见的动态变量就是header中的各种校验参数,token最为常见,一般接口提供方会额外提供一个token请求接口,配合postman的Pre-request Scripts进行使用
pm.test("Status code is 200", function () {
pm.response.to.have.status(200);
});
// 把responseBody转为json字符串
var data = JSON.parse(responseBody);
// 设置环境变量token,供后面的接口引用,位置就是上一步获取的位置
pm.environment.set("access_token", data.result.access_token);
2.设置测试方法
2.1 全局测试
对所有接口来说,共通就是返回体结构测试,即保证code正确、数据不为空
在collection中进行Tests添加
pm.test("HTTP请求 200", function () {
pm.response.to.have.status(200);
});
var jsonData = pm.response.json();
pm.test("接口返回200", function () {
pm.expect(jsonData.resultCode === 200).to.equal(true);
});
pm.test("接口返回数据不为空", function () {
if(Array.isArray(jsonData.result)){
pm.expect(jsonData.result.length > 0).to.equal(true);
}else{
pm.expect(jsonData.result !== null).to.equal(true);
}
});
2.2 接口schema校验
- json转json schema
postman内置了Ajv JSON schema validator
的校验方式,为了方便起见,我们需要把接口设计阶段定义的json转为json schema,然后通过pm.response.to.have.jsonSchema
进行校验
- 如接口定义阶段,json模拟数据如下
{
"resultCode": 200,
"resultMsg": "success",
"result": {
"basicData": {
"shipCode": "与四山立百",
"shipNo": "但命及风他",
"shipRegNo": "最离矿则它设历",
"shipFirstregNo": "光具样任般高时",
"origShipRegNo": "构天当斯消然",
"shipId": "她提圆性识",
"origShipName": "根得取特水应干",
"shipIdFlagCode": "了细十产",
"shipInspectNo": "布人关油速热酸",
"shipImo": "行家适青",
"shipMmsi": "马铁至土治那",
"shipCallsign": "所目入口安华日",
"shipName": "向等代手研",
"shipNameEn": "接上决压除",
"origShipNameEn": "头接治",
"shipRegionFlagCode": "机石家毛党布",
"shipRouteCode": "更与林几器",
"sailAreaCode": "根设眼实值还",
"regportCode": "格重安程速",
"origRegportName": "结中上时",
"shipHullMaterialCode": "片流便包还众",
"shipTypeCode": "备满发直安外能",
"shipValue": "线革商再",
"shipLength": 39657,
"shipBreadth": 56071,
"shipDepth": 98131,
"shipGrosston": 93962,
"shipNetton": 55891,
"shipDwt": "斯取声对律公人",
"shipEngineTypeCode": "然此已认",
"shipEngineNum": 5815,
"shipEnginePower": 20505,
"shipPropellerTypeCode": "适较八自情放",
"shipPropellerNum": 81206,
"shipSlotNum": 63116,
"shipParkNum": 23051,
"shipPassengerNum": 91588,
"shipSummerDraft": 1200,
"shipWindLevel": "号话用",
"shipMinFreeboard": 30004,
"shipyard": "造口放将信下",
"shipyardEn": "米选斯计改",
"shipBuiltAddr": "转七入见发",
"shipBuiltAddrEn": "空千律认商",
"shipBuiltDate": "1983-05-14 10:06:54",
"rebuiltShipyard": "点常主反行南类",
"rebuiltShipyardEn": "也么以府有",
"shipRebuiltAddr": "而是六水",
"shipRebuiltAddrEn": "第自们断况",
"icCardNo": "养听维点算办",
"origDeletionDate": "1991-06-28 12:11:21",
"shipRebuiltDate": "1985-04-07 13:17:08",
"statusFlagCode": "矿行市正亲电",
"shipIdSealFlagCode": "说亲变内",
"mortgageFlagCode": "快需大半圆证",
"bareboatFlagCode": "中位阶也论",
"alterFlagCode": "江业查",
"handoutCardFlagCode": "地王圆收究求",
"financialLeaseFlagCode": "以主总上器东",
"hibernateFlagCode": "意眼应处",
"trialShipFlagCode": "选热上",
"detainFlagCode": "型开所积",
"permanentSealRemark": "光定部王手命动",
"orgCode": "还行保在即",
"shipRouteCodeCn": "列照她你",
"shipReginFlagCodeCn": "河",
"sailAreaCodeCn": "连速老江她",
"shipTypeCodeCn": "内九量劳关",
"shipRegionFlagCodeCn": "段示线所",
"regportCodeCn": "化石值究",
"shipEngineTypeCodeCn": "高构克他群",
"shipPropellerTypeCodeCn": "个动所快理",
"orgCodeCn": "商张说器形"
}
}
}
- 通过在线工具进行转换,json schema转换
{
"type": "object",
"required": [],
"properties": {
"resultCode": {
"type": "number"
},
"resultMsg": {
"type": "string"
},
"result": {
"type": "object",
"required": [],
"properties": {
"basicData": {
"type": "object",
"required": [],
"properties": {
"shipCode": {
"type": "string"
},
"shipNo": {
"type": "string"
},
"shipRegNo": {
"type": "string"
},
"shipFirstregNo": {
"type": "string"
},
"origShipRegNo": {
"type": "string"
},
"shipId": {
"type": "string"
},
"origShipName": {
"type": "string"
},
"shipIdFlagCode": {
"type": "string"
},
"shipInspectNo": {
"type": "string"
},
"shipImo": {
"type": "string"
},
"shipMmsi": {
"type": "string"
},
"shipCallsign": {
"type": "string"
},
"shipName": {
"type": "string"
},
"shipNameEn": {
"type": "string"
},
"origShipNameEn": {
"type": "string"
},
"shipRegionFlagCode": {
"type": "string"
},
"shipRouteCode": {
"type": "string"
},
"sailAreaCode": {
"type": "string"
},
"regportCode": {
"type": "string"
},
"origRegportName": {
"type": "string"
},
"shipHullMaterialCode": {
"type": "string"
},
"shipTypeCode": {
"type": "string"
},
"shipValue": {
"type": "string"
},
"shipLength": {
"type": "number"
},
"shipBreadth": {
"type": "number"
},
"shipDepth": {
"type": "number"
},
"shipGrosston": {
"type": "number"
},
"shipNetton": {
"type": "number"
},
"shipDwt": {
"type": "string"
},
"shipEngineTypeCode": {
"type": "string"
},
"shipEngineNum": {
"type": "number"
},
"shipEnginePower": {
"type": "number"
},
"shipPropellerTypeCode": {
"type": "string"
},
"shipPropellerNum": {
"type": "number"
},
"shipSlotNum": {
"type": "number"
},
"shipParkNum": {
"type": "number"
},
"shipPassengerNum": {
"type": "number"
},
"shipSummerDraft": {
"type": "number"
},
"shipWindLevel": {
"type": "string"
},
"shipMinFreeboard": {
"type": "number"
},
"shipyard": {
"type": "string"
},
"shipyardEn": {
"type": "string"
},
"shipBuiltAddr": {
"type": "string"
},
"shipBuiltAddrEn": {
"type": "string"
},
"shipBuiltDate": {
"type": "string"
},
"rebuiltShipyard": {
"type": "string"
},
"rebuiltShipyardEn": {
"type": "string"
},
"shipRebuiltAddr": {
"type": "string"
},
"shipRebuiltAddrEn": {
"type": "string"
},
"icCardNo": {
"type": "string"
},
"origDeletionDate": {
"type": "string"
},
"shipRebuiltDate": {
"type": "string"
},
"statusFlagCode": {
"type": "string"
},
"shipIdSealFlagCode": {
"type": "string"
},
"mortgageFlagCode": {
"type": "string"
},
"bareboatFlagCode": {
"type": "string"
},
"alterFlagCode": {
"type": "string"
},
"handoutCardFlagCode": {
"type": "string"
},
"financialLeaseFlagCode": {
"type": "string"
},
"hibernateFlagCode": {
"type": "string"
},
"trialShipFlagCode": {
"type": "string"
},
"detainFlagCode": {
"type": "string"
},
"permanentSealRemark": {
"type": "string"
},
"orgCode": {
"type": "string"
},
"shipRouteCodeCn": {
"type": "string"
},
"shipReginFlagCodeCn": {
"type": "string"
},
"sailAreaCodeCn": {
"type": "string"
},
"shipTypeCodeCn": {
"type": "string"
},
"shipRegionFlagCodeCn": {
"type": "string"
},
"regportCodeCn": {
"type": "string"
},
"shipEngineTypeCodeCn": {
"type": "string"
},
"shipPropellerTypeCodeCn": {
"type": "string"
},
"orgCodeCn": {
"type": "string"
}
}
}
}
}
}
}
- 根据接口实际情况,修改上述schema的具体定义
如string字段可为空
原始定义:
"orgCodeCn": {
"type": "string"
}
修改后:
"orgCodeCn": {
"type": ["string","null"]
}
如时间格式是YYYY-MM-DD
原始定义:
"origDeletionDate": {
"type": "string"
}
修改后:
"origDeletionDate": {
"type": "string",
"pattern":"(([0-9]{3}[1-9]|[0-9]{2}[1-9][0-9]{1}|[0-9]{1}[1-9][0-9]{2}|[1-9][0-9]{3})-(((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01]))|((0[469]|11)-(0[1-9]|[12][0-9]|30))|(02-(0[1-9]|[1][0-9]|2[0-8]))))|((([0-9]{2})(0[48]|[2468][048]|[13579][26])|((0[48]|[2468][048]|[3579][26])00))-02-29)"
}
具体schema的其他属性和相关说明,可参考 JSON Schema规范(中文版)
注意:pattern用的是正则表达式,一些常用的正则在网上搜索一下,二次校验即可。没必要自己花大量精力造轮子。
3.批量运行
- 先运行登录接口,更新token至全局变量
- 泡杯热茶,运行整个collection
- 查看运行结果,一眼便知道哪些接口有问题了
4.不同环境的处理办法
导出collection文件至内网实际环境,需要更换全局变量即可
内部保护->prometheus http埋点监控
在微服务中,网关服务可以完成对系统内接口的监控和流量检测,但很多时候,对复杂的业务系统来说,还需要对接外部的服务,硬编码rest请求处理。
如何保证系统可以持续观测外部接口的状态和处理时间呢,这里推荐使用springboot+prometheus的方式,对系统内的接口进行埋点监测,对异常的访问,配合
alertmanager+webhook
进行告警实时通知。
1.pom.xml
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<version>1.8.5</version>
</dependency>
2.配置restTemplate
这里将系统内的restTemplate
的默认HTTP请求先修改为OKHTTP
@Bean
public RestTemplate restTemplate(MeterRegistry registry) {
// 先配置OKhttpClient,添加eventListener收集okhttpClient指标
OkHttpClient client = new OkHttpClient
.Builder()
.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(), SSLSocketClient.getX509TrustManager())
.hostnameVerifier(SSLSocketClient.getHostnameVerifier())
.eventListener(OkHttpMetricsEventListener
.builder(registry, "okhttp.requests")
.uriMapper(req -> req.url().encodedPath())
.tags(Tags.of("okhttp", "performance"))
.build())
.build();
OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory(client);
// 通过配置文件配置即可,注入代码省略
factory.setConnectTimeout(connectTimeout);
factory.setReadTimeout(readTimeout);
factory.setWriteTimeout(writeTimeout);
return new RestTemplate(factory);
}
@Bean
public OkHttpClient okHttpClient(MeterRegistry registry) {
return new OkHttpClient.Builder()
.eventListener(OkHttpMetricsEventListener.builder(registry, "okhttp.requests")
.tags(Tags.of("okhttp", "performance"))
.build())
.build();
}
3.micrometer配置
@Configuration
public class MicroMeterConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> meterRegistryCustomizer(@Value("${spring.application.name}") String applicationName) {
return meterRegistry -> meterRegistry
.config()
.commonTags(Collections.singletonList(Tag.of("application", applicationName)));
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
4.添加actuator暴露端点
management:
endpoints:
web:
exposure:
include: '*'
配置完成后,采用传统的方式进行使用即可
@Autowired
private RestTemplate restTemplate;
5.prometheus.yml
添加配置
- job_name: 'spring_grafana'
scrape_interval: 5s
scrape_timeout: 4s
metrics_path: 'actuator/prometheus'
static_configs:
- targets: ['ip:port']
6.grafana
添加dashboard
Spring Boot 2.1 System Monitor
最终效果
链接汇总