大家好,我是米洛,求三连!求关注测试开发坑货!

回顾

上一节我们引入了AceEditor帮助我们在线执行/调试SQL语句,这一届我们讲点儿断言相关的内容。

数据比对

接口测试中,我们常常需要对接口的返回参数进行校验。如果采用数据驱动的方式,涉及到多组入参/出参的比对的情况下,怎么对预期json和实际json进行一个全方位的对比呢?

今天,他来了。

其实这个话题很早以前源自于虫师selenium群大师兄精哥的指点,而我和饭佬,属于偷师成功的典范。

不过我在这个基础上也加了一点自己的东西,最终效果是一样的。

核心点

这个算法的核心点就在于递归,一层一层去递归,最终达到拿到所有差异的过程。

具体一点

具体一点,假设我们现在有2个json对象,分别是:

  1. # 预期结果
  2. a = """
  3. {
  4. "name": "lixiaoyao",
  5. "age": 19,
  6. "wife": ["linyueru", "zhaolinger"],
  7. "job": {
  8. "yuhang": "混混",
  9. "suzhou": "林家堡姑爷",
  10. "suoyaota": "仙剑派弟子"
  11. }
  12. }
  13. """
  14. # 实际结果
  15. b = """
  16. {
  17. "name": "lixiaoyao",
  18. "age": 23,
  19. "wife": ["anu", "zhaolinger"],
  20. "job": {
  21. "yuhang": "混混",
  22. "suzhou": "林家堡姑爷",
  23. "suoyaota": "仙剑派子弟"
  24. }
  25. }
  26. """

不仔细看还挺难发现这里面的差异,因为json内容不算很少,所以肉眼比较难看出来。

那么我们何不利用代码,去帮我们智能对比呢?

步步为营

确认参数

首先我们需要知道我们要比对什么,其实就是2个string,但他们是JSON格式的。

所以我们可以确定好2个基本参数: def compare(a: str, b: str)

  • a 预期结果

  • b 实际结果
    但是我们需要深层次比对,所以我们需要额外传入一些数据:

  • ans
    用来存放比对的信息,比如 123 != 124

  • path
    这个用来存放当前的路径,比如上述例子的job->suoyaota这个地方的值就不一样,一个是仙剑派弟子,另一个是仙剑派子弟,所以我们不但要记录值,还要记录他的路径。

思考

在Python中,数据结构较为简单,我们看看JSON序列化JSONDecoder类
就能大概知道了:

测试平台系列(56) JSON深层次对比方案 - 图1

可以看到,基本上json的数据类型能够对应的,我们可以再简化一下:

能继续深入对比不能继续深入对比这2种。

什么意思呢?

比如a和b的name都是lixiaoyao,lixiaoyao是个字符串,当它不为json字符串的时候,是一个不能继续深入对比的数据。

所以此时我们的递归到这一层就应该终止,直接比对a和b的name字段,如果不一样,根据path,把diff结果添加到ans中。

那什么又是可深入比较对象呢?我认为有3种:

  1. List

Python的数组里面可以继续遍历,里面还有可能继续有json数据,所以可继续对比。

  1. Dict

这个不用多说了,大家都知道这个是最容易疯狂嵌套的。

  1. JSON字符串

注意他其实是字符串的一种,只不过他能被反序列化为可继续遍历的对象。

编写转换为Python对象的方法

  1. def _to_json(string):
  2. try:
  3. float(string)
  4. return string
  5. except:
  6. try:
  7. if isinstance(string, str):
  8. return json.loads(string)
  9. return string
  10. except:
  11. return string

首先我们拿到的数据是我们期望它是一个字符串,我们最先判断它是不是数值类型,如果是,直接返回这个字符串。

为什么呢?因为这个字符串如果是数值类型,那么他已经确定不可继续遍历了,我们把它原路返回。

但因为他也可能不是字符串而是Python对象比如dict或者其他数据,所以我们接着判断他是不是字符串,如果能被反序列化又不是数值的话,那说明他就是JSON字符串,如果通通不是,那我们把数据原路返回。

这一步只是为了筛选出字符串内容为JSON的数据,如果不是则直接返回之前的数据。

编写_compare核心方法

  1. def _compare(self, a, b, ans, path):
  2. a = self._to_json(a)
  3. b = self._to_json(b)
  4. if type(a) != type(b):
  5. ans.append(f"{self._weight(path)} 类型不一致, 分别为{type(a)} {type(b)}")
  6. return
  7. if isinstance(a, dict):
  8. keys = []
  9. for key in a.keys():
  10. pt = path + "/" + key
  11. if key in b.keys():
  12. self._compare(a[key], b[key], ans, pt)
  13. keys.append(key)
  14. else:
  15. ans.append(f"{self._weight(pt)} 在后者中不存在")
  16. for key in b.keys():
  17. if key not in keys:
  18. pt = path + "/" + key
  19. ans.append(f"{self._weight(pt)} 在后者中多出")
  20. elif isinstance(a, list):
  21. i = j = 0
  22. while i < len(a):
  23. pt = path + "/" + str(i)
  24. if j >= len(b):
  25. ans.append(f"{self._weight(pt)} 在后者中不存在")
  26. i += 1
  27. j += 1
  28. continue
  29. self._compare(a[i], b[j], ans, pt)
  30. i += 1
  31. j += 1
  32. while j < len(b):
  33. pt = path + "/" + str(j)
  34. ans.append(f"{self._weight(pt)} 在前者中不存在")
  35. j += 1
  36. else:
  37. if a != b:
  38. ans.append(
  39. f"{self._weight(path)} 数据不一致: {JsonService._color(a)} "
  40. f"!= {self._color(b, 1)}" if path != "" else
  41. f"数据不一致: {self._color(a)} != {JsonService._color(b, 1)}")

先用_to_json转为Python对象,获得ab。接着判断他们的类型是否一致,如果不一致则没必要继续比较了,比如一个是list,另一个是dict,根本没有比较的意义,直接ans.append错误信息即可,记得带上path参数。

self._weight是为了在html日志中更好地展示效果,加了一些style样式,可以先忽略。

如果类型也一致了,我们继续来看a是什么类型。

  • 如果是字典
    我们的比较是以a(预期结果)为单位的,所以一切以a为标准。
    那么我们遍历a和b的keys,分别找出a字典里面有,b字典没有的key,和b字典里面有,而a字典里面没有的key。
    注意,这里代码可以简化,字典的keys是支持集合操作的,交由大家思考优化。
    中间去遍历了a和b都有key,然后继续调用了self._compare方法,并把path改为了path+”/“+key,这样的话路径就为字典的深一层的路径了,继续递归调用。

  • 如果是list
    与dict其实类似,定义了2个指针,依次走完2个数组,当a数组已经走完了,b里面还有值,就把b里面剩下的值(属于多出的信息)都添加到错误信息之中。
    其中也获取了新的path,只不过数组是用的索引,而dict用的是key作为路径。
    接着递归。。。

  • 如果不是这2种
    注意这里是递归结束的条件,那我们直接比较。数据不一致,则把不一致的数据写到ans数组中。


大体思路就是这样,给大家看看color和weight。

测试平台系列(56) JSON深层次对比方案 - 图2

测验刚才的结果

测试平台系列(56) JSON深层次对比方案 - 图3

可以看到age不一样,老婆不一样,锁妖塔的职业也不一样。所以,你学费了吗?

提高点

优化字典之间的key

最终源码+测试代码

  1. import json
  2. class JsonCompare:
  3. def compare(self, exp, act):
  4. ans = []
  5. self._compare(exp, act, ans, '')
  6. return ans
  7. def _compare(self, a, b, ans, path):
  8. a = self._to_json(a)
  9. b = self._to_json(b)
  10. if type(a) != type(b):
  11. ans.append(f"{path} 类型不一致, 分别为{type(a)} {type(b)}")
  12. return
  13. if isinstance(a, dict):
  14. keys = []
  15. for key in a.keys():
  16. pt = path + "/" + key
  17. if key in b.keys():
  18. self._compare(a[key], b[key], ans, pt)
  19. keys.append(key)
  20. else:
  21. ans.append(f"{pt} 在后者中不存在")
  22. for key in b.keys():
  23. if key not in keys:
  24. pt = path + "/" + key
  25. ans.append(f"{pt} 在后者中多出")
  26. elif isinstance(a, list):
  27. i = j = 0
  28. while i < len(a):
  29. pt = path + "/" + str(i)
  30. if j >= len(b):
  31. ans.append(f"{pt} 在后者中不存在")
  32. i += 1
  33. j += 1
  34. continue
  35. self._compare(a[i], b[j], ans, pt)
  36. i += 1
  37. j += 1
  38. while j < len(b):
  39. pt = path + "/" + str(j)
  40. ans.append(f"{pt} 在前者中不存在")
  41. j += 1
  42. else:
  43. if a != b:
  44. ans.append(
  45. f"{path} 数据不一致: {a} "
  46. f"!= {b}" if path != "" else
  47. f"数据不一致: {a} != {b}")
  48. def _color(self, text, _type=0):
  49. if _type == 0:
  50. # 说明是绿色
  51. return """<span style="color: #13CE66">{}</span>""".format(text)
  52. return """<span style="color: #FF4949">{}</span>""".format(text)
  53. def _weight(self, text):
  54. return """<span style="font-weight: 700">{}</span>""".format(text)
  55. def _to_json(self, string):
  56. try:
  57. float(string)
  58. return string
  59. except:
  60. try:
  61. if isinstance(string, str):
  62. return json.loads(string)
  63. return string
  64. except:
  65. return string
  66. if __name__ == "__main__":
  67. # 预期结果
  68. a = """
  69. {
  70. "name": "lixiaoyao",
  71. "age": 19,
  72. "wife": ["linyueru", "zhaolinger"],
  73. "job": {
  74. "yuhang": "混混",
  75. "suzhou": "林家堡姑爷",
  76. "suoyaota": "仙剑派弟子"
  77. }
  78. }
  79. """
  80. # 实际结果
  81. b = """
  82. {
  83. "name": "lixiaoyao",
  84. "age": 23,
  85. "wife": ["anu", "zhaolinger"],
  86. "job": {
  87. "yuhang": "混混",
  88. "suzhou": "林家堡姑爷",
  89. "suoyaota": "仙剑派子弟"
  90. }
  91. }
  92. """
  93. obj = JsonCompare()
  94. ans = obj.compare(a, b)
  95. print(ans)