測試平臺系列(56) JSON深層次對比方案
大家好,我是米洛,求三連!求關注
測試開發坑貨
!
回顧
上一節我們引入了AceEditor
幫助我們線上執行/除錯SQL語句,這一屆我們講點兒斷言
相關的內容。
資料比對
在介面測試
中,我們常常需要對介面的返回引數進行校驗
。如果採用資料驅動的方式,涉及到多組入參/出參的比對的情況下,怎麼對預期json和實際json進行一個全方位的對比呢?
今天,他來了。
其實這個話題很早以前源自於蟲師selenium群
,大師兄
對精哥
的指點,而我和飯佬,屬於偷師成功
的典範。
不過我在這個基礎上也加了一點自己的東西,最終效果是一樣的。
核心點
這個演算法的核心點就在於遞迴,一層一層去遞迴,最終達到拿到
所有差異的過程。
具體一點
具體一點,假設我們現在有2個json物件,分別是:
# 預期結果 a = """ { "name": "lixiaoyao", "age": 19, "wife": ["linyueru", "zhaolinger"], "job": { "yuhang": "混混", "suzhou": "林家堡姑爺", "suoyaota": "仙劍派弟子" } } """ # 實際結果 b = """ { "name": "lixiaoyao", "age": 23, "wife": ["anu", "zhaolinger"], "job": { "yuhang": "混混", "suzhou": "林家堡姑爺", "suoyaota": "仙劍派子弟" } } """
不仔細看還挺難發現這裡面的差異
,因為json內容不算很少,所以肉眼比較難看出來。
那麼我們何不利用程式碼,去幫我們智慧對比
呢?
步步為營
確認引數
首先我們需要知道我們要比對什麼,其實就是2個string,但他們是JSON格式
的。
所以我們可以確定好2個基本引數: def compare(a: str, b: str)
-
a 預期結果
-
b 實際結果
但是我們需要深層次比對,所以我們需要額外傳入一些資料:
-
ans
用來存放比對的資訊,比如 123 != 124
-
path
這個用來存放當前的路徑,比如上述例子的job->suoyaota這個地方的值就不一樣,一個是
仙劍派弟子
,另一個是仙劍派子弟
思考
在Python中,資料結構較為簡單,我們看看JSON序列化JSONDecoder類
就能大概知道了:
可以看到,基本上json的資料型別能夠對應的,我們可以再簡化一下:
能繼續深入對比
和 不能繼續深入對比
這2種。
什麼意思呢?
比如a和b的name都是lixiaoyao,lixiaoyao是個字串
,當它不為json字串的時候,是一個不能繼續深入對比
的資料。
所以此時我們的遞迴到這一層就應該終止,直接比對a和b的name欄位,如果不一樣,根據path,把diff結果新增到ans中。
那什麼又是可深入比較物件呢?我認為有3種:
- List
Python的數組裡面可以繼續遍歷,裡面還有可能繼續有json資料,所以可繼續對比。
- Dict
這個不用多說了,大家都知道這個是最容易瘋狂巢狀
的。
- JSON字串
注意他其實是字串的一種,只不過他能被反序列化為可繼續遍歷
的物件。
編寫轉換為Python物件的方法
def _to_json(string):
try:
float(string)
return string
except:
try:
if isinstance(string, str):
return json.loads(string)
return string
except:
return string
首先我們拿到的資料是我們期望它
是一個字串,我們最先判斷它是不是數值型別,如果是,直接返回這個字串。
為什麼呢?因為這個字串如果是數值型別,那麼他已經確定不可繼續遍歷了,我們把它原路返回。
但因為他也可能不是字串而是Python物件比如dict或者其他資料,所以我們接著判斷他是不是字串,如果能被反序列化又不是數值的話,那說明他就是JSON字串
,如果通通不是,那我們把資料原路返回。
這一步只是為了篩選出字串內容為JSON的資料,如果不是則直接返回之前的資料。
編寫_compare核心方法
def _compare(self, a, b, ans, path):
a = self._to_json(a)
b = self._to_json(b)
if type(a) != type(b):
ans.append(f"{self._weight(path)} 型別不一致, 分別為{type(a)} {type(b)}")
return
if isinstance(a, dict):
keys = []
for key in a.keys():
pt = path + "/" + key
if key in b.keys():
self._compare(a[key], b[key], ans, pt)
keys.append(key)
else:
ans.append(f"{self._weight(pt)} 在後者中不存在")
for key in b.keys():
if key not in keys:
pt = path + "/" + key
ans.append(f"{self._weight(pt)} 在後者中多出")
elif isinstance(a, list):
i = j = 0
while i < len(a):
pt = path + "/" + str(i)
if j >= len(b):
ans.append(f"{self._weight(pt)} 在後者中不存在")
i += 1
j += 1
continue
self._compare(a[i], b[j], ans, pt)
i += 1
j += 1
while j < len(b):
pt = path + "/" + str(j)
ans.append(f"{self._weight(pt)} 在前者中不存在")
j += 1
else:
if a != b:
ans.append(
f"{self._weight(path)} 資料不一致: {JsonService._color(a)} "
f"!= {self._color(b, 1)}" if path != "" else
f"資料不一致: {self._color(a)} != {JsonService._color(b, 1)}")
先用_to_json轉為Python物件,獲得a
和b
。接著判斷他們的型別是否一致,如果不一致則沒必要
繼續比較了,比如一個是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。
測驗剛才的結果
可以看到age不一樣,老婆不一樣,鎖妖塔的職業
也不一樣。所以,你學費了嗎?
提高點
優化字典之間的key
最終原始碼+測試程式碼
import json
class JsonCompare:
def compare(self, exp, act):
ans = []
self._compare(exp, act, ans, '')
return ans
def _compare(self, a, b, ans, path):
a = self._to_json(a)
b = self._to_json(b)
if type(a) != type(b):
ans.append(f"{path} 型別不一致, 分別為{type(a)} {type(b)}")
return
if isinstance(a, dict):
keys = []
for key in a.keys():
pt = path + "/" + key
if key in b.keys():
self._compare(a[key], b[key], ans, pt)
keys.append(key)
else:
ans.append(f"{pt} 在後者中不存在")
for key in b.keys():
if key not in keys:
pt = path + "/" + key
ans.append(f"{pt} 在後者中多出")
elif isinstance(a, list):
i = j = 0
while i < len(a):
pt = path + "/" + str(i)
if j >= len(b):
ans.append(f"{pt} 在後者中不存在")
i += 1
j += 1
continue
self._compare(a[i], b[j], ans, pt)
i += 1
j += 1
while j < len(b):
pt = path + "/" + str(j)
ans.append(f"{pt} 在前者中不存在")
j += 1
else:
if a != b:
ans.append(
f"{path} 資料不一致: {a} "
f"!= {b}" if path != "" else
f"資料不一致: {a} != {b}")
def _color(self, text, _type=0):
if _type == 0:
# 說明是綠色
return """<span style="color: #13CE66">{}</span>""".format(text)
return """<span style="color: #FF4949">{}</span>""".format(text)
def _weight(self, text):
return """<span style="font-weight: 700">{}</span>""".format(text)
def _to_json(self, string):
try:
float(string)
return string
except:
try:
if isinstance(string, str):
return json.loads(string)
return string
except:
return string
if __name__ == "__main__":
# 預期結果
a = """
{
"name": "lixiaoyao",
"age": 19,
"wife": ["linyueru", "zhaolinger"],
"job": {
"yuhang": "混混",
"suzhou": "林家堡姑爺",
"suoyaota": "仙劍派弟子"
}
}
"""
# 實際結果
b = """
{
"name": "lixiaoyao",
"age": 23,
"wife": ["anu", "zhaolinger"],
"job": {
"yuhang": "混混",
"suzhou": "林家堡姑爺",
"suoyaota": "仙劍派子弟"
}
}
"""
obj = JsonCompare()
ans = obj.compare(a, b)
print(ans)