转转QA 转转流量录制与回放 - 回放结果算法降噪

庄锦弟 · 2024年02月21日 · 308 次阅读

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

文章章节

一、业务背景
二、思考
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 种:业务异常非业务异常

  • 非业务异常:作为重点排除对象,把这类异常问题全部排除掉,对回放结果的干扰降到最低,甚至为 0,例如下图,是 repeater 服务调用目标回放服务引发的异常问题,并非业务本身报错异常,判断为异常是无效异常

无效异常数据

  • 业务异常:需要区分出真的异常和 “假的异常”,过滤掉大量 “假的异常”,需要做排除处理,控制这类异常问题干扰在个位数以内

    • 有效结果:例如下图,录制正常返回数据,回放时结果为空,diff失败结果有效

有效diff失败数据

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

有效diff失败数据

  • 无效结果:例如下图,并非真正的业务异常,只是查询结果返回不一致,属于正常业务响应结果,判断为diff失败是无效异常

    无效diff失败数据

2、如何判断异常结果?

  • 人工一条条数据分析查看,得出结果成功与否

  • 回放结果列表根据类型拉取 diff 失败和异常的数据

  • 查看服务日志是否有异常报错

  • 查看业务场景回放数据是否正确

  • 拿请求参数单接口重新测试,再对比结果

  • ……

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

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

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

结果状态?

结果值的差异?

参数 key 的差异?

响应文本的差异?

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

三、探索相似度模型

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

1、参考算法

杰卡德相似系数

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

杰卡德系数

杰卡德距离

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

杰卡德距离

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

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

  1. 编辑距离(Levenshtein 距离)
    • 公式:计算将一个字符串转换成另一个字符串所需的最少编辑操作次数。
    • 原理:通过比较两个字符串(在这里是 JSON 字符串),编辑距离公式可以度量从一个字符串变为另一个字符串所需要的最小修改次数。这些修改可能包括添加、删除或替换一个字符。
  2. Jaccard 相似度
    • 公式:Jaccard 相似度 = (两个集合的交集大小) / (两个集合的并集大小)。
    • 原理:Jaccard 相似度是一个衡量两个集合相似度的指标。对于 JSON 对象,我们可以将其视为键值对的集合,Jaccard 相似度可以帮助我们理解两个 JSON 对象在键值对集合方面的相似性。
  3. 余弦相似度
    • 公式:余弦相似度 = 两个向量的夹角的余弦值。
    • 原理:余弦相似度通常用于衡量两个向量之间的相似性。在比较 JSON 对象时,我们可以将每个对象转换为一个向量,其中每个维度的值对应一个键值对的值。然后,我们可以使用余弦相似度来比较这两个向量,从而理解两个 JSON 对象在值方面的相似性。

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

3、设计思路

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

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

  • 原理:这种方法基于计算两个 JSON 对象中对应值的差异,然后根据这些差异计算出一个百分比。这个百分比表示了两个对象在值上的相似度。
  • 公式:使用差异度量公式,差异百分比 = (差异的总数 / 总数) * 100%
  • 应用场景:当你需要比较两个 JSON 对象的值时,例如在数据质量检查、数据审计或数据同步的场景中。
  • 解决什么问题:这种校验可以快速了解两个对象在值上的差异程度,从而可以决定是否接受、拒绝或进一步处理这些差异。

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

  • 原理:这种方法关注于 JSON 对象的键(key)。如果两个对象有相同的键,并且对应值不为空,那么它们被视为相同。基于这个信息,可以计算出一个百分比来表示键的相似度。
  • 公式:键的相似度百分比 = (相同键的数量 / 总键数) * 100%
  • 应用场景:当你的关注点是确保 JSON 对象中有关键的数据元素,并且这些元素在两个对象中都存在时。
  • 解决什么问题:这种校验可以发现丢失或新增的键,以及那些键对应的值是否为空。这有助于确保数据的完整性。

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

  • 原理:这种方法评估两个 JSON 对象的结构是否相似或相同。如果结构相同,那么可以认为这两个对象是相似的。
  • 公式:使用二元判断,结构相似度百分比 = 100% - (结构差异的数量 / 总结构数量) * 100%
  • 应用场景:当你需要确保两个 JSON 对象的结构保持一致时,例如在数据交换、数据集成或数据转换的场景中。
  • 解决什么问题:这种校验可以发现结构上的差异,从而帮助你识别可能的格式问题、不一致性或数据错误。

4、流程设计

1.分析和计算规则

  • 最终相似度计算公式:(值相似度 * 0.2 + 键相似度 * 0.4 + 结构完整性相似度 * 0.4)* 100% = 最终相似度

为什么采用此计算公式?

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

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

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

  • 系统 diff:只要存在结果不一致则为失败,相等则为成功

  • 相似度算法:校验结果值差异,参数 key 差异,数据结构完整性差异,三者计算结果乘以系数得出最终百分比,按照百分比分段分析数据结果,小于 80%(大于等于 60% 小于 80%,为疑似异常,小于 60% 为异常)则为失败,大于或等于 80%(大于等于 80% 小于 90%,为疑似异常,大于 90% 为正常)则为成功

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、结构完整性,无法判断结果数据是属于什么业务场景,以及业务场景是否属于正常响应

已知不足的两个问题点:

  • 录制响应结果的整体结构与回放响应结果的整体结构不一致,且结果值也存在不一致,分析结果不一定准确

  • 相同参数,因业务场景流转,前后返回结果不一样,影响校验规则计算,分析结果不一定准确

后续……

  • 探索更多应用场景,将算法应用于更多领域,发挥其更大的价值。

  • 虽然算法能够校准确分析两个 json 差异,但在处理大规模数据时效率还是会比较低,需要进一步优化算法以提高性能。

  • 目前的相似度分析机制还较为简单,仅基于两个 json 对象差异进行计算,偏程序化,不能动态判断业务场景结果,未来会考虑引入更多特征以提高算法分析的准确性。

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


关于作者

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

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

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册