我正在参加「掘金·启航计划」
经常在技术群里或者各种博客中看到大家在争执API测试工具应该用哪个,什么http client,postman,apifox,jmeter,那该如何选择适合自己的工具呢,如何才能提高生产力,更快的做完手上工作呢,其实,归根到底,没有最好,只有适合自己的才是最好的,在接口开发的不同阶段,选择适合自己的工具,来更快的实现目标就行。
CURD多年,前后台测试接口,跨团队测试接口,后台调用外部接口,经常性的会出现各种各样的扯皮现象,主要来来回回就是这几个:
- 接口格式不对
- 接口字段不对
- 接口缺失字段
- 接口不够规范
- 接口不够稳定
那,该如何避免或者节省这之间的来来回回的开销呢?
设计
-
定规范
-
确定请求方式,常用的
GETPOST,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