1. 程式人生 > 其它 >測試平臺系列(56) JSON深層次對比方案

測試平臺系列(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種:

  1. List

Python的數組裡面可以繼續遍歷,裡面還有可能繼續有json資料,所以可繼續對比。

  1. Dict

這個不用多說了,大家都知道這個是最容易瘋狂巢狀的。

  1. 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物件,獲得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。

測驗剛才的結果

可以看到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)