原创声明:转载请附上原文出处链接

文章章节

一、业务背景
二、思考
1、如何区分结果为有效或无效?
2、如何判断异常结果?
3、结果对比到底对比什么?
三、探索相似度模型
1、参考算法
2、参考数学公式
3、设计思路
4、流程设计
四、相似度算法试验
1、测试数据
2、校验结果值的差异
3、校验 key 的差异
4、校验数据结构完整性的差异
5、测试结果
五、回放结果降噪
六、总结
1、验证结论
2、优点与不足

一、业务背景

流量回放时出现大量 diff 失败的数据,排查和分析时,发现大量 diff 失败并非真正业务场景触发的失败,异常数据的干扰影响非常大,无法直接判断结果正确与否,对业务使用的体验性比较差

如何做到 diff 失败的数据不会有非失败数据的干扰?

diff失败数据

无效diff失败数据

二、思考

1、如何区分结果为有效或无效?

有效结果:属于业务服务相关的异常问题,为有效结果

无效结果:非业务服务相关的异常问题,为无效结果

先把已知道的异常类型全部列举出来:

1、执行回放的服务造成目标服务报错
2、回放的目标服务报错
3、回放的目标服务依赖的服务报错
4、回放结果数据转换报错
5、回放结果的响应报文存在异常
6、录制的流量响应结果存在异常
7、回放结果响应报文与录制的响应报文存在数据差异
8、回放环境服务不存在
9、回放结果超时
……

异常类型很多,要一个分析很浪费时间和精力,从列举的异常类型分析,整体可以划分为 2 种:业务异常非业务异常

无效异常数据

有效diff失败数据

录制结果响应异常,回放结果响应正常,diff失败结果有效

有效diff失败数据

2、如何判断异常结果?

以上做法,不论是采用哪种,都不是高效且又快速能得出可靠结果的方式,每个任务排期都是相对固定的,再投入多余时间去分析很多无差别的数据,脑壳不够用,完全不够用!相比之下,可能人工测试的结果来得更可靠和直观些,至少能直接知道结果有效与否

不禁陷入深思,流量回放的能力本来是要提高工作效率的,最终却因结果分析反而成为负担 …… …… …… 疯狂抓头发!!!有画面感吧,虽然离秃还差那么3000发丝

3、结果对比到底对比什么?

结果状态?

结果值的差异?

参数 key 的差异?

响应文本的差异?

感觉只对比哪个结果也不一定对,又感觉几项都缺一不可……

三、探索相似度模型

基于思考的方向,探索比对两个结果的模型,查阅大量文献之后,初步设计相似度模型:

1、参考算法

杰卡德相似系数

两个集合 A 和 B 交集元素的个数在 A、B 并集中所占的比例,称为这两个集合的杰卡德系数,用符号 J(A,B) 表示。杰卡德相似系数是衡量两个集合相似度的一种指标(余弦距离也可以用来衡量两个集合的相似度)。

杰卡德系数

杰卡德距离

杰卡德距离 (Jaccard Distance) 是用来衡量两个集合差异性的一种指标,它是杰卡德相似系数的补集,被定义为 1 减去 Jaccard 相似系数。

杰卡德距离

杰卡德距离用两个集合中不同元素占所有元素的比例来衡量两个集合的区分度。

2、参考数学公式及其原理

  1. 编辑距离(Levenshtein 距离)
  2. Jaccard 相似度
  3. 余弦相似度

这些公式和原理都是为了帮助我们更准确地比较和评估两个 JSON 对象的相似性或差异性。

3、设计思路

参考以上的算法和数学公式,主要有三个方向的规则设计:

1.value -- value:校验结果值的差异,得出百分比

2.key -- key:校验 key 的差异,只要相同 key,且结果值不为空,则理解为相同,得出最终百分比

3.structure -- structure:校验 json 结构完整性,结构相同则为相似,得出百分比

4、流程设计

1.分析和计算规则

为什么采用此计算公式?

1)值相似度系数 0.2:因为录制数据存在不同来源环境,且数据变化比较大,而回放环境只是测试环境,相同请求参数的条件下,最终响应结果可能存在不同结果值比较大,为兼容多种条件,故调整结果值的系数占比为最低

2)键相似度系数 0.4:评估同一个接口录制前端响应数据的 key 差异性,若前后变化很大,则说明业务有变化或者接口结构有变更

3)结构完整性相似度系数 0.4:同上

2.diff 逻辑

diff逻辑

3.diff 结果分析

diff分析

四、相似度算法试验

1、测试数据

需要对比差异的两个 json 文本

String jsonString1 = "{\"result\":{\"code\":0,\"data\":[{\"activityId\":\"123\",\"activityLevelDesc\":\"C\",\"activityStatusDesc\":\"未开始\",\"activityTypeDesc\":\"大促\",\"allActivityTime\":null,\"activityName\":\"活动N数不清了\",\"cateInfo\":\"手机、其他网络设备\",\"activityStatus\":0,\"canSignUp\":1,\"activityTime\":[\"2024年01月23日 15:43-2024年01月25日 10:38\",\"2024年01月26日 15:43-2024年01月27日 10:38\"],\"activityType\":1,\"activityLevel\":4,\"activity\":4},{\"activityId\":\"124\",\"activityLevelDesc\":\"B-\",\"activityStatusDesc\":\"进行中\",\"activityTypeDesc\":\"日常活动\",\"allActivityTime\":null,\"activityName\":\"kftest0116\",\"cateInfo\":\"手机\",\"activityStatus\":1,\"canSignUp\":1,\"activityTime\":[\"2024年01月16日 14:04-2024年01月31日 14:04\"],\"activityType\":0,\"activityLevel\":4},{\"activityId\":\"125\",\"activityLevelDesc\":\"A\",\"activityStatusDesc\":\"进行中\",\"activityTypeDesc\":\"日常活动\",\"allActivityTime\":null,\"activityName\":\"kftest01221720\",\"cateInfo\":\"笔记本、手机\",\"activityStatus\":1,\"canSignUp\":1,\"activityTime\":[\"2024年01月22日 17:21-2024年01月24日 17:21\"],\"activityType\":0,\"activityLevel\":2}],\"success\":true,\"cookieValue\":null,\"raw\":false,\"errorMsg\":null,\"errorJsonMsg\":\"\"},\"exception\":null}";

String jsonString2 = "{\"result\":{\"code\":0,\"data\":[{\"activityId\":\"123\",\"activityLevelDesc1\":\"C\",\"activityStatusDesc\":\"未开始\",\"activityTypeDesc\":\"大促\",\"allActivityTime\":null,\"activityName\":\"活动N数不清了\",\"cateInfo\":\"手机、其他网络设备\",\"activityStatus\":0,\"canSignUp\":1,\"activityTime\":[\"2024年01月23日 15:43-2024年01月25日 10:38\",\"2024年01月26日 15:43-2024年01月27日 10:38\"],\"activityType\":1,\"activityLevel\":6},{\"activityId\":\"124\",\"activityLevelDesc\":\"B-\",\"activityStatusDesc\":\"进行中\",\"activityTypeDesc\":\"日常活动\",\"allActivityTime\":null,\"activityName\":\"kftest0116\",\"cateInfo\":\"手机\",\"activityStatus\":1,\"canSignUp\":1,\"activityTime\":[\"2024年01月16日 14:04-2024年01月31日 14:04\"],\"activityType\":0,\"activityLevel\":5},{\"activityId\":\"125\",\"activityLevelDesc\":\"A\",\"activityStatusDesc\":\"进行中\",\"activityTypeDesc\":\"日常活动\",\"allActivityTime\":null,\"activityName\":\"kftest01221720\",\"cateInfo\":\"笔记本、手机\",\"activityStatus\":1,\"canSignUp\":1,\"activityTime\":[\"2024年01月22日 17:21-2024年01月24日 17:21\"],\"activityType\":0,\"activityLevel\":2}],\"success\":true,\"cookieValue\":null,\"raw\":false,\"errorMsg\":null,\"errorJsonMsg\":\"{\\\"errorMsg\\\":null}\"},\"exception\":null}";

两个字符串转为 JSONObject 对象


public boolean resultOfContrast(String recordResponse, String replayResponse){

    JSONObject json1 = new JSONObject(recordResponse);
    JSONObject json2 = new JSONObject(replayResponse);
    ……

以录制的响应结果为作为基准(json1),对比回放的响应结果(json2),得出两者最终相似度结果。

2、计算两个 json 字符串的值是否相等

判断两个 json 对象所有结果值,首先把两个 json 对象进行序列化处理,取出所有值,返回两个 map 对象再一一对比,得出最终值的相似度百分比

/**
 * 对比两个json字符串的值是否相等,并计算出两个json字符串的相似度
 * @param json1
 * @param json2
 * @return
 */
public static BigDecimal compareResultValues(JSONObject json1, JSONObject json2) {
    Map<String, Object> allValues1 = findAllValues(json1);
    Map<String, Object> allValues2 = findAllValues(json2);

    // int totalKeys = allValues1.size() + allValues2.size();
    int totalKeys = allValues1.size();
    int matchedKeys = 0;

    for (Map.Entry<String, Object> entry : allValues1.entrySet()) {
        if (allValues2.containsKey(entry.getKey())) {
            Object value1 = entry.getValue();
            Object value2 = allValues2.get(entry.getKey());

            if (value1 != null && value2 != null && isValueEqual(value1, value2)) {
                matchedKeys++;
            }
        }
    }

    double similarityPercentage =  (double) matchedKeys / totalKeys;
    return BigDecimal.valueOf(similarityPercentage).setScale(4, RoundingMode.HALF_UP);
}



private static boolean isValueEqual(Object value1, Object value2) {
    if (value1 instanceof JSONObject && value2 instanceof JSONObject) {
        return compareJsonObjects((JSONObject) value1, (JSONObject) value2);
    } else if (value1 instanceof JSONArray && value2 instanceof JSONArray) {
        return compareJsonArrays((JSONArray) value1, (JSONArray) value2);
    } else {
        return value1.equals(value2);
    }
}

private static boolean compareJsonObjects(JSONObject json1, JSONObject json2) {
    if (json1.length() != json2.length()) {
        return false;
    }

    for (String key : json1.keySet()) {
        if (!json2.has(key)) {
            return false;
        }

        Object value1 = json1.get(key);
        Object value2 = json2.get(key);

        if (!isValueEqual(value1, value2)) {
            return false;
        }
    }

    return true;
}

private static boolean compareJsonArrays(JSONArray array1, JSONArray array2) {
    if (array1.length() != array2.length()) {
        return false;
    }

    for (int i = 0; i < array1.length(); i++) {
        Object value1 = array1.get(i);
        Object value2 = array2.get(i);

        if (!isValueEqual(value1, value2)) {
            return false;
        }
    }

    return true;
}

private static Map<String, Object> findAllValues(JSONObject json) {
    Map<String, Object> values = new HashMap<>();
    findAllValues("", json, values);
    return values;
}

private static void findAllValues(String prefix, JSONObject json, Map<String, Object> values) {
    for (String key : json.keySet()) {
        String path = prefix.isEmpty() ? key : prefix + "." + key;
        Object value = json.get(key);

        if (value instanceof JSONObject) {
            findAllValues(path, (JSONObject) value, values);
        } else if (value instanceof JSONArray) {
            JSONArray array = (JSONArray) value;
            for (int i = 0; i < array.length(); i++) {
                Object item = ((JSONArray) value).get(i);
                if (item instanceof JSONObject) {
                    JSONObject jsonObject = (JSONObject) item;
                    findAllValues(path + "[" + i + "]", jsonObject, values);
                }else {
                    values.put(path, value);
                }
            }
        } else {
            values.put(path, value);
        }
    }
}



3、计算两个 json 中所有非空的 key 的相似度

判断两个 json 对象非空 key,首先把两个 json 对象进行序列化处理,再所有 key(对象路径,key 路径),返回两个 map 对象再一一对比,得出最终 key 的相似度百分比

/**
 * 获取json中所有非空的key路径,并进行对比,同时计算出相似度
 * @param json1
 * @param json2
 * @return
 */
public static BigDecimal compareNonEmptyKeys(JSONObject json1, JSONObject json2) {
    Map<String, List<String>> nonEmptyKeyPaths1 = findNonEmptyKeyPaths(json1);
    Map<String, List<String>> nonEmptyKeyPaths2 = findNonEmptyKeyPaths(json2);

    // 使用流和 Collectors.toSet() 来获取所有键的集合
    Set<String> keysInBothMaps = nonEmptyKeyPaths1.keySet().stream()
            .filter(nonEmptyKeyPaths2::containsKey)
            .collect(Collectors.toSet());

    int totalNonEmptyKeys = nonEmptyKeyPaths1.size();
    int matchedNonEmptyKeys = (int) keysInBothMaps.stream()
            .filter(key -> nonEmptyKeyPaths1.get(key).equals(nonEmptyKeyPaths2.get(key)))
            .count();


    double similarityPercentage =  (double) matchedNonEmptyKeys / totalNonEmptyKeys;
    return BigDecimal.valueOf(similarityPercentage).setScale(4, RoundingMode.HALF_UP);
}

4、计算两个 json 字符串的结构完整的相似度

采用的是二元判断的方式,先把两个 json 对象进行序列化解析,JSONObject 对比 JSONObject,JSONArray 对比 JSONArray,key 对 key,返回两个 map 对象,再计算最终结构相似度,结构相同则为 100%,否则对比两个 json 对象结构相似度的百分比,最终取两者交集的百分比作为结构完整性的相似度结果。

/**
 * 对比两个json字符串的结构完整度,并计算出相似度
 * @param json1
 * @param json2
 * @return
 */
public static BigDecimal compareStructuralIntegrity(JSONObject json1, JSONObject json2) {
    Map<String, List<String>> structure1 = flattenJsonStructure(json1);
    Map<String, List<String>> structure2 = flattenJsonStructure(json2);

    int forwardTotalKeys = structure1.size();
    int forwardCount = 0;

    for (Map.Entry<String, List<String>> entry : structure1.entrySet()) {
        if (structure2.containsKey(entry.getKey())) {
            forwardCount++;
        }
    }


    int reverseTotalKeys = structure2.size();
    int reverseCount = 0;

    for (Map.Entry<String, List<String>> entry : structure2.entrySet()) {
        if (structure1.containsKey(entry.getKey())) {
            reverseCount++;
        }
    }

    // 正向百分比
    double forwardPercentage = (double) forwardCount / forwardTotalKeys;
    // 反向百分比
    double reversePercentage = (double) reverseCount / reverseTotalKeys;
    // 比较大小,取交集的结果,交集即是小的值
    double smallerPercentage = Math.min(forwardPercentage, reversePercentage);
    return BigDecimal.valueOf(smallerPercentage).setScale(4, RoundingMode.HALF_UP);
}

5、测试结果

测试结果

五、回放结果降噪

对比系统 diff 和相似度算法,结合实际回放数据进行分析,以下数据为某一案例分析结果:

对比项 回放总 case 数 diff 失败总数 diff 失败实际数 diff 失败中判断正确的比例 diff 结果正确率
系统 diif 667 328 188 57.32% 53.35%
相似度算法 667 218 188 86.24% 86.89%
人工判断结果 667 188 188 100% 100%

【结果分析】

总共 328 条 diff 失败数据,相似度算法降噪之后还剩下 218 条 diff 失败,其中有 43 条分析结果不准确,通过相似度算法 328-218=110 个回放结果被正确识别为成功(系统 diff 为失败),且与人工判断一致

系统 diff 正确率:(328-(110+43))/328 = 53.35%

相似度算法正确率:(328-43)/328 = 86.89%

【结果示例】

成功列表和 diff 失败列表

成功结果

失败结果

疑似异常详情:

失败详情

成功详情

六、总结

1、验证结论

【算法实现】

成功实现了系统 diff 对比相似度算法,能够准确比较两个系统之间的差异并输出最终相似度

【测试验证】

通过大量测试用例验证了算法的准确性和稳定性,证明了其在实际应用中的可行性。

【应用场景实践】

当大批流量回放结果,业务逻辑是正确的,但结果又是 Diff 失败的 Case,这些 Case 从业务角度分析不应该作为失败的 Case。这样的结果导致整体的成功率较低,同时加大了失败 Case 的排查难度。(比如,单据状态变更了,表单信息被修改了等等)

基于以上问题,经相似度算法分析之后,对干扰数据做降噪处理,减少无效 diff 失败的 Case,同时降低失败 Case 的排查难度,提高回放成功率,最终体现回放结果价值。

2、优点和不足

【优点】

1、结果数据对比可以根据不同维度的差异,进行分析和计算,得出最终有效结果,具备一定的筛查能力。

2、对比系统 diff 能力,准确率更高,且正确率高出 30% 以上。

3、可动态配置计算比例,更灵活校验结果有效性。

【不足】

目前只判断结果值、参数 key、结构完整性,无法判断结果数据是属于什么业务场景,以及业务场景是否属于正常响应

已知不足的两个问题点:

后续……

流量回放平台外传……
老板:快速用 1 万条数据去覆盖业务场景,下班前做完
你:人工点点点…… …… ……何时休?
流量回放平台:要不来我这试试


关于作者

庄锦弟,负责测试平台一体化能力基建

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转 FE」(专注于 FE)、「转转 QA」(专注于 QA),更多干货实践,欢迎交流分享~


↙↙↙阅读原文可查看相关链接,并与作者交流