质量体系建设之路可视化的MockServer

一、 背景

福禄网络作为一家数字权益商品及服务提供商,覆盖了我们衣食住行的各种生活场景的权益内容,对接了如支付宝、京东、银行APP各种渠道,如何能够快速的响应渠道需求,提供稳定的接口服务,这就要求我们电商团队能够做到比渠道快一步的接口测试;
同时作为一家集团化的公司,内部的信息化系统对接了众多银行的相关支付业务,涉及到查余额、下流水、支付、对账等日常资金业务,这要求信息化部门能够确保资金支付相关场景能够在上线前进行完整覆盖,业务方新的业务接入或者需求场景变更比较频繁,版本的快速迭代背景下如何保证众多的场景能够快速覆盖,通过完全真实的业务操作成本是巨大的;

二、 引入MOCK

基于上述的业务系统测试痛点,质量管理团队决定引入mock服务。我们首先想到的是以最低的成本来完成,市面上有许多的mockserver的开源软件,但在调研了相关的开源产品之后,我们发现没有一款比较贴合我们业务需求的产品;
比如我们的资金支付相关场景对接的银行方,都是以xml报文的格式作为请求和返回;有些场景对返回模板的数据是动态要求的,比如某个支付状态第一次请求是处理中,第N次请求变为成功;而有些银行通信协议是socket等,通过调研后我们决定自己来开发一套mock服务。

三、框架选型

  1. 界面可视化操作
    为了保持系统风格的统一,我们决定沿用QECS平台的前端框架(QECS详细介绍移步至:https://www.cnblogs.com/fulu/p/15419208.html)
  2. 高性能
    为了满足高性能的需求,模板数据存储我们采用redis;开始我们准备沿用QECS的后端框架flask,在进行了一系列的性能测试后,我们发现无法支撑我们5KTPS的需求,后来我们改用了springBoot,经过测试能够达到我们的需求;

四、具体实现

4.1 设计方案

20211126150028.png
外部请求打入Mock服务,监听服务获取到请求通过Redis中的已有模板进行规则匹配,满足匹配规则返回对应模板数据;不满足返回无法匹配的数据提示。

4.2 功能说明

数据格式
目前匹配规则支持json、form、query、xml这几种参数类型;返回支持json、xml和text这几种数据类型
状态码
可以模拟返回200、500、304、502、503、400等各种http状态码
返回时间
可以设置请求mock服务后的返回间隔时间,对超时返回场景比较有效
动态取值
设置了内置函数和从匹配规则中取值这两种方式,可以动态设置返回数据的字段值
自定义代码
对于平台界面暂时无法支撑的个性化需求,可以通过在Hermes中通过代码来实现,灵活方便
高性能
模板数据通过Redis进行存储,请求进来到模拟返回通过redis进行匹配,能够提供高性能的返回,可作为性能压测的挡板服务

4.3 平台功能介绍

20211126150136.png
4.3.1 主界面
20211126150156.png

4.3.2 创建API,即我们需要模拟调用的对方接口
20211126150218.png

4.3.3 配置模板,配置API的期望模板
20211126150241.png
模板数据会存储到REDIS
20211126150304.png

4.3.4 模拟请求
获取实际请求值,获取期望模板值,将模板与请求值进行比较匹配,当模板请求参数属于真实请求参数的子集,匹配成功
20211126150323.png

4.3.5 自定义函数的实现
为了满足个性化的使用场景,服务内置了一批内置函数来满足动态取值的场景

private static String getReplaceStr(String str){
    if(str.contains("Random")){
        String[] strA = str.trim().split(",");
        int i1 = strA[0].indexOf("(");
        int i2 = strA[1].indexOf(")");
        String min = strA[0].substring(i1+1);
        String max = strA[1].substring(0,i2);
        return getRandom(Integer.valueOf(min), Integer.valueOf(max)).toString();
    }
    if(str.contains("CurrentDate")){
        return getCurrentDate();
    }
    if(str.contains("SecondTime")){
        return getSecondTime().toString();
    }
    if(str.contains("MicTime")){
        return getMicTime().toString();
    }
    if(str.contains("CardID")){
        return getCardID();
    }
    if(str.contains("Phone")){
        return getPhone();
    }
    return str;
}

20211126150359.png
20211126150418.png

4.3.6 专家模式,对于目前界面暂时无法支撑的个性化需求,我们采取通过在后端服务编码的方式进行处理

@RequestMapping(value = {"/test"})
public void loadNum(HttpServletRequest request, HttpServletResponse httpServletResponse){
    TimerTool timerTool = new TimerTool();
    RestfulRequest restfulRequest = requestService.analyseRequest(request);
    restfulRequest = filterHeader(restfulRequest);
    log.info("分析请求cost:{}", timerTool.cut());

    List<MockInfo> mockRules = mockInfoService.getMockRules(restfulRequest.getPath());
    log.info("获取mock配置记录:{}, cost:{}", mockRules, timerTool.cut());

    MockInfo mockRule = mockService.matchRulesNew(restfulRequest, mockRules);
    log.info("匹配tag...cost:{}", timerTool.cut());
    //先走平台模板,如果匹配不到,走专家模式(代码实现)的MOCK匹配逻辑
    if(mockRule != null){
        mockService.doResponseNew(mockRule, httpServletResponse);
    }else {
        mockExpertService.doResponse(restfulRequest, httpServletResponse);
    }
}

专家模式实现个性化的mock逻辑

public void doResponse(RestfulRequest request, HttpServletResponse response){
    try {

        RestfulBody body = request.getBody();
        String ip = request.getIp();
        JSONObject requestParams = body.getParams();
        String funName = requestParams.getJSONObject("CMBSDKPGK").getJSONObject("INFO").getString("FUNNAM");

        Integer responseStatus = 200 ;
        Integer responseSleep = 0;
        String responseString = "mock数据去火星了!";
        String responseInfo = mockInfoService.getMockExpert(ip,funName);
        JSONObject responseInfoJson;

        if(StringUtils.isEmpty(responseInfo)){
            responseInfoJson = new JSONObject(true);
            responseInfoJson.put("count", 0);
            responseInfoJson.put("firstTime", TimeUtils.nowTimeStamp() / 1000);
            responseInfoJson.put("response", getJsonResponse("xmlResource/resp_" + funName + ".xml"));
        }else {
            responseInfoJson = JSONObject.parseObject(responseInfo);
        }
        int count = responseInfoJson.getInteger("count");
        JSONObject responseObj = responseInfoJson.getJSONObject("response");
        // 批量查询余额
        if("NTQADINF".equals(funName)){
            long time_lag = (TimeUtils.nowTimeStamp() / 1000) -  responseInfoJson.getLong("firstTime");
            Object obj = responseObj.getJSONObject("CMBSDKPGK").get("NTQADINFZ");
            // 距离第一次请求超过60s 余额+5000
            if(time_lag > 60){
                if(obj instanceof JSONArray){
                    JSONArray jsonA = (JSONArray)obj;
                    for(int i=0; i<jsonA.size();i++){
                        Double avlblv_value = jsonA.getJSONObject(i).getDoubleValue("AVLBLV");
                        avlblv_value = avlblv_value + 5000;
                        jsonA.getJSONObject(i).put("AVLBLV", String.format("%.2f",avlblv_value));
                    }
                    responseObj.put("NTQADINFZ", jsonA);
                }
                if(obj instanceof JSONObject){
                    JSONObject jsono = (JSONObject)obj;
                    Double avlblv_value = jsono.getDoubleValue("AVLBLV");
                    avlblv_value = avlblv_value + 5000;
                    jsono.put("AVLBLV", String.format("%.2f",avlblv_value));
                    responseObj.put("NTQADINFZ", jsono);
                }
            }
        };

五、未来展望

目前这个版本我们适配了http协议下的大部分数据格式,如上所述,对于一些个性化的需求我们暂时采取的专家模式代码实现,一方面体现了自有平台的灵活性,另一方面我们也会在不断的使用过程中进行抽象提炼成共性的需求继续可视化的实现。
内置函数随着平台的使用范围和深度,相信会有更多需求场景需要相关函数的实现加以满足,不断去丰富这些函数,也是平台健壮的一个表现;
另外对于其他相关协议的补充也是平台需要进行后续规划完善的发力点。
你们有哪些mock的实际使用场景,欢迎交流

福禄·研发中心 福小龙
原文地址:https://www.cnblogs.com/fulu/p/15637885.html