質量體系建設之路---視覺化的MockServer

福祿網路研發團隊發表於2021-12-03

一、 背景

福祿網路作為一家數字權益商品及服務提供商,覆蓋了我們衣食住行的各種生活場景的權益內容,對接瞭如支付寶、京東、銀行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的實際使用場景,歡迎交流

福祿·研發中心 福小龍

相關文章