今天,我们将聚焦自动化测试的三大核心之一,被誉为“守门员”的关键角色——断言器。在自动化测试中,断言器负责验证系统输出是否符合预期,是确保测试有效性的核心环节。
然而,面对格式多样、结构复杂的测试数据(尤其是主流的 JSON 格式),传统的断言方式往往显得效率低下、维护困难,如:
效率低下: 手动编写断言代码工作量大,容易出错,难以应对海量测试数据。
维护困难: 接口响应结构变化时,需要手动修改大量断言代码,维护成本高。
可读性差: 断言代码冗长复杂,难以理解和维护,影响测试用例的可读性和可维护性。
为了解决这些痛点,我们将深入探索 JSON 断言器的设计与实现,揭秘如何构建一个高效、灵活、易维护的 JSON 断言器,为您的自动化测试保驾护航!
JSON对比断言器,从功能上大概划分如下几个部分:
KuMatchers 作为断言器工具集合的入口,封装了丰富的断言器,能够与各种自动化测试框架无缝集成,在测试执行的任何阶段,轻松完成实际结果与预期数据的对比验证。
// 与本地文件对比
actual KuMatchers.equalToJsonInClassPath(expectFile)
// 与对象对比
actual KuMatchers.equalToJson(expect)
.withLabel("报告标签")
// 使用正则表达式指定忽略对比的 JSON 路径。
.ignorePathsRegexes(["/test1"])
// 设置浮点数对比的精度位数。
.withDecimalPlaces(2)
// 标记该断言用于异步测试场景。
.markAsAsyncTest()
// 开启严格模式,JSON 结构或字段不匹配时断言失败。
.enableStrictMode()
// 指定数组元素的排序规则,确保对比结果一致。
.setArraySorting([
"/path1": ["key1","key2"],
"/path2": ["key1", "key2"],
"/path3": [],]
)
# 自动创建预期结果文件
matcher.json.compare.expect.auto.create=true
# 覆盖预期结果文件 (谨慎使用)
matcher.json.compare.expect.overwrite=false
# 测试成功时覆盖预期结果
matcher.json.compare.expect.overwrite.if.success=false
# 测试失败时覆盖预期结果
matcher.json.compare.expect.overwrite.if.fail=false
# 严格模式 (新增字段或对象会导致断言失败)
matcher.json.compare.strict=false
# 全局忽略路径正则表达式 (对所有测试用例生效)
# matcher.json.compare.ignore.regex=/b, /subModels/.*/id
#....等等
2、可视化入口
2种常见的使用场景
集成测试报告:断言器将断言结果通过数据管理模块上传至服务器,并在测试报告中嵌入可视化页面。
独立页面访问:通过独立的页面入口,手动输入 JSON 数据和选择对比规则,实时查看可视化结果,进行数据分析和规则调试。
页面集成丰富的可视化功能
定义对比规则:灵活定义数据对比规则,包括忽略路径、排序规则、精度设置等,满足不同场景下的对比需求。
结果分享:轻松分享可视化结果,方便团队成员协作分析和问题讨论。
可视化索引:建立可视化结果索引,方便快速查找和定位历史对比记录。
规则前端编辑:提供友好的前端界面,方便您直接在页面上编辑和调试对比规则,提升规则管理效率。
规则设计思路
传统的断言框架通常使用简单的布尔值标记断言结果(通过或失败),这限制了结果的表达能力和可视化可能性。为了在页面可视化和断言器中使用统一的规则,需要选择将可视化规则(断言结果数据)作为对比核心的输出结果。这些规则经过后置处理,可以转换为可视化所需的格式或断言器需要的结果。
参考 JSON Patch (RFC 6902) 的规范,使用以下操作描述两个 JSON 对象 (a 和 b) 之间的差异:
add: b 相对于 a,需要在指定路径 (path) 添加值 (value)。
remove: b 相对于 a,需要删除指定路径 (path) 的值。
replace: b 相对于 a,需要将指定路径 (path) 的值替换为目标值 (value)。
路径 (path) 使用 JSON Pointer (RFC 6901) 表示:
https://www.rfc-editor.org/rfc/rfc6901
断言结果判定
如果 a 和 b 的对比结果生成的非空规则列表,则说明存在差异,断言结果为 false
。
如果规则列表为空,则说明 a 和 b 完全一致,断言结果为 true
。
规则案例
[
{"op":"add", "path":"/0/a", "value":1},
{"op":"remove","path":"/0/a","value":1},
{"op":"replace","path":"/0/a","toPath":"/0/a","value":2,"toValue":1}
]
规则优势
统一规则: 可视化页面和断言器使用相同的规则,保证一致性。
灵活表达: 支持多种操作类型,可以描述复杂的 JSON 差异。
易于解析: 基于 JSON 格式,方便解析和处理。
为了满足不同场景下的数据对比需求,需要提供灵活的前置和后置处理规则。这些规则主要分为两类:
影响对比输入数据的规则:
应用场景: 当需要更新预期结果或进行数据覆写时,需要决定是否使用处理后的数据。
注意事项: 在页面可视化时,需要明确标注使用的是原始数据还是处理后的数据,避免混淆。
不影响对比输入数据的规则:
应用场景: 一般用于对对比结果进行过滤、排序等操作,不影响原始数据。
注意事项: 无需特殊处理,可直接应用于对比结果。
代码示例:
public class JsonDiffSettings {
/**
* 后置处理,源数据无变化
* 忽略校验的 json point路径。
*/
List<String> ignoreJsonPathRegexps = [];
/**
* 后置处理,源数据无变化
* 是否为严格模式,严格模式add认为对比失败。
*/
boolean strict = false;
/**
* 前置处理,源数据存在
* 小数精度。
*/
Integer decimalPrecision = null;
...等等
}
四、核心算法
对比核心是 JSON对比 的核心模块,负责对前置处理后的数据进行深度对比,并输出符合基础可视化规则的对比结果。为了更清晰地展示数据差异,我们将 JSON 节点值类型分为两类,并分别将其映射为基础的可视化结果,假设存在两个 JSON 对象 a 和 b,其中 a 为预期结果,b 为实际结果。
a 和 b 相同的 key:递归对比对应的 value 值。
a 中多出的 key:标记为 "remove",表示预期结果中存在但实际结果中缺失的字段。
b 中多出的 key:标记为 "add",表示实际结果中存在但预期结果中缺失的字段。
迭代 a 的所有 key:
迭代 b 的所有 key:
数组对比常见的有两种数组对比策略,可以基于实际情况选择实现
基于数组坐标对比: 按照数组下标依次对比每个元素,性能较好,但当数组元素顺序不一致时,会导致较多的不一致结果,影响可视化效果。
寻找两个数组之间的最长公共子序列,并以此为基础进行对比,可以更清晰地展示数组元素的差异,但性能相对较差。
2、性能对比
数组元素顺序固定: 建议使用 基于数组坐标对比,以获得更好的性能。不考虑可视化的情况下数组大小对比会更快。
数组元素顺序不固定: 建议使用 优先查找公共序列对比,以获得更清晰的可视化效果。
实际使用了 优先查找公共序列对比 的方案,并采用了动态规划算法来求解最长公共子序列。以下是简化后的 Groovy 伪代码:
// 状态转移方程
def dp(i, j) {
if (a[i-1] == b[j-1]) {
return dp(i-1, j-1) + 1
} else {
return Math.max(dp(i, j-1), dp(i-1, j))
}
}
// 状态回溯
def backtrackDiffs(a, b, dp) {
def arrayChanges = []
def aSize = a.size(), bSize = b.size()
def aChangeCnt = 0, bChangeCnt = 0
while (aSize > 0 || bSize > 0) {
if (aSize > 0 && bSize > 0) {
if (a[aSize-1] == b[bSize-1]) {
// 记录变更
recordChanges(arrayChanges, aChangeCnt, bChangeCnt, aSize-1, bSize-1)
aChangeCnt = 0
bChangeCnt = 0
aSize--
bSize--
} else if (dp[aSize-1][bSize] > dp[aSize][bSize-1]) {
aChangeCnt++
aSize--
} else {
bChangeCnt++
bSize--
}
} else if (aSize > 0) {
aChangeCnt++
aSize--
} else {
bChangeCnt++
bSize--
}
// 处理剩余变更
if (aSize == 0 && bSize == 0) {
recordChanges(arrayChanges, aChangeCnt, bChangeCnt, -1, -1)
aChangeCnt = 0
bChangeCnt = 0
}
}
return arrayChanges.reverse()
}
五、页面可视化
页面可视化功能基于 svelte-jsoneditor 组件实现,该组件提供了丰富的 JSON 数据展示和编辑功能。为了更直观地展示数据差异,我们进行了以下优化:
实现原理: 利用 onClassName
方法,根据对比核心生成的规则,对存在差异的 JSON 节点进行高亮显示。
高亮规则:
新增节点: 使用绿色背景高亮。
删除节点: 使用红色背景高亮。
修改节点: 使用黄色背景高亮。
实现原理: 利用 scrollTo
方法,根据对比核心生成的规则,快速定位到存在差异的 JSON 节点。
索引规则:
支持按路径索引: 通过 JSON Path,快速定位到指定节点。
支持按差异类型索引: 选择差异类型(新增、删除、修改),快速定位到所有对应类型的节点。
通过本文介绍自动化测试中JSON断言器的设计与实现,基本解决了传统断言方式在效率、维护性和可读性上的不足,通过架构设计、核心算法和可视化功能提升了断言器的灵活性和易用性。未来我们还可以在效率、扩展方面不断扩展,如: