前言
記錄Json斷言在作業中的應用進階,
很早之前
很早以前寫過一篇博客,記錄當時獲取一個多級json中指定key的資料:
#! /usr/bin/python
# coding:utf-8
"""
@author:Bingo.he
@file: get_target_value.py
@time: 2017/12/22
"""
def get_target_value(key, dic, tmp_list):
"""
:param key: 目標key值
:param dic: JSON資料
:param tmp_list: 用于存盤獲取的資料
:return: list
"""
if not isinstance(dic, dict) or not isinstance(tmp_list, list): # 對傳入資料進行格式校驗
return 'argv[1] not an dict or argv[-1] not an list '
if key in dic.keys():
tmp_list.append(dic[key]) # 傳入資料存在則存入tmp_list
for value in dic.values(): # 傳入資料不符合則對其value值進行遍歷
if isinstance(value, dict):
get_target_value(key, value, tmp_list) # 傳入資料的value值是字典,則直接呼叫自身
elif isinstance(value, (list, tuple)):
_get_value(key, value, tmp_list) # 傳入資料的value值是串列或者元組,則呼叫_get_value
return tmp_list
def _get_value(key, val, tmp_list):
for val_ in val:
if isinstance(val_, dict):
get_target_value(key, val_, tmp_list) # 傳入資料的value值是字典,則呼叫get_target_value
elif isinstance(val_, (list, tuple)):
_get_value(key, val_, tmp_list) # 傳入資料的value值是串列或者元組,則呼叫自身
后來在寫用例生成程序中,發現重復的斷言描述資訊較多,大多數資料回傳其實都是標準的json,所以將整個回傳的json放到斷言資料中:
continue_run_flag = True
def assert_json(base, juge, contain=(), reg=()):
# 回傳值,不符合預期時會設定為False
flag = True
for key, value in base.items():
# 不進行斷言的資料不進一步處理
if key not in juge:
continue
if key in COMMON_RE_CHECK:
if not re.match(COMMON_RE_CHECK[key], base[key]):
flag = False
logger.error("欄位[{}]使用通用的正則匹配,不符合預期:預期正則匹配【{}】== 【{}】".format(key, COMMON_RE_CHECK[key], juge[key]))
else:
logger.warning("欄位【{}】使用通用欄位的正則匹配, 符合預期".format(key))
continue
if key in contain:
if str(value) not in juge[key]:
flag = False
logger.error("欄位[{}]不符合預期:預期【{}】包含 【{}】".format(key, juge[key], value))
continue
logger.info("欄位[{}]符合預期:預期[{}] 包含 [{}]".format(key, juge[key], value))
continue
if key in reg:
if not re.match(juge[key], value):
flag = False
logger.error("欄位[{}]不符合預期:預期正則匹配【{}】== 【{}】".format(key, value, juge[key]))
continue
logger.info("欄位[{}]斷言成功:預期[{}]== 實際[{}]".format(key, value, juge[key]))
continue
if isinstance(value, str) or isinstance(value, int):
if juge[key] != value:
logger.error("欄位[{}]不符合預期:預期【{}】!= 實際【{}】".format(key, value, juge[key]))
flag = False
continue
elif isinstance(value, dict):
assert_json(value, juge[key], contain=contain, reg=reg)
elif isinstance(value, list):
for i, v in enumerate(value):
if isinstance(value, str) or isinstance(value, int):
if v != juge[key][i]:
logger.error("欄位[{}]不符合預期:預期【{}】!= 實際【{}】".format(key, value, juge[key]))
else:
logger.info("欄位[{}]斷言成功:預期[{}]== 實際[{}]".format(key, value, juge[key]))
elif isinstance(value, dict):
assert_json(value[i], juge[key][i], contain=contain, reg=reg)
else:
assert_json(value[i], juge[key][i], contain=contain, reg=reg)
else:
pass
logger.info("欄位[{}]符合預期: 預期[{}]== 實際[{}]".format(key, value, juge[key]))
# 失敗是否繼續執行,默認為TRUE
if not continue_run_flag:
assert flag
return flag
呼叫:
rsp = requests.get('http://localhost:8800/get', params={"name": "bingo", "age": 18}).json()
assert_json(rsp, {
"args": {
"name": "bingo"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Cache-Control": "max-age=259200",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.27.1",
"X-Amzn-Trace-Id": r"Root=\d-\w{8}-\w{24}"
},
"req_param": [
{
"name": "bingo",
"age": "18"
}
],
"origin": r"",
"url": "http://httpbin.org/get?name=bingo"
},
contain=(), reg=("X-Amzn-Trace-Id", "origin",))
日志效果:
2022-05-05 14:25:49.967 | INFO | __main__:assert_json:173 - 欄位[name]符合預期: 預期[bingo]== 實際[bingo]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:173 - 欄位[args]符合預期: 預期[{'name': 'bingo'}]== 實際[{'name': 'bingo'}]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:173 - 欄位[Accept]符合預期: 預期[*/*]== 實際[*/*]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:173 - 欄位[Accept-Encoding]符合預期: 預期[gzip, deflate]== 實際[gzip, deflate]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:173 - 欄位[Cache-Control]符合預期: 預期[max-age=259200]== 實際[max-age=259200]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:173 - 欄位[Host]符合預期: 預期[httpbin.org]== 實際[httpbin.org]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:173 - 欄位[User-Agent]符合預期: 預期[python-requests/2.27.1]== 實際[python-requests/2.27.1]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:149 - 欄位[X-Amzn-Trace-Id]斷言成功:預期[Root=1-62734553-430db0707e1a3656043cd165]== 實際[Root=\d-\w{8}-\w{24}]
2022-05-05 14:25:49.968 | INFO | __main__:assert_json:173 - 欄位[headers]符合預期: 預期[{'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Cache-Control': 'max-age=259200', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.27.1', 'X-Amzn-Trace-Id': 'Root=1-62734553-430db0707e1a3656043cd165'}]== 實際[{'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Cache-Control': 'max-age=259200', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.27.1', 'X-Amzn-Trace-Id': 'Root=\\d-\\w{8}-\\w{24}'}]
2022-05-05 14:25:49.969 | WARNING | __main__:assert_json:133 - 欄位【origin】使用通用欄位的正則匹配, 符合預期
2022-05-05 14:25:49.969 | INFO | __main__:assert_json:173 - 欄位[age]符合預期: 預期[18]== 實際[18]
2022-05-05 14:25:49.969 | INFO | __main__:assert_json:173 - 欄位[name]符合預期: 預期[bingo]== 實際[bingo]
2022-05-05 14:25:49.969 | INFO | __main__:assert_json:173 - 欄位[req_param]符合預期: 預期[[{'age': '18', 'name': 'bingo'}]]== 實際[[{'name': 'bingo', 'age': '18'}]]
2022-05-05 14:25:49.969 | INFO | __main__:assert_json:173 - 欄位[url]符合預期: 預期[http://httpbin.org/get?name=bingo]== 實際[http://httpbin.org/get?name=bingo]
封裝
將方法簡單封裝到呼叫類中:
class HttpBin:
def __init__(self):
self.continue_run_flag = True # 失敗是否繼續執行
self.base_url = 'http://localhost:8800'
self.base_param = {"local_class": self.__class__.__name__}
def get(self, param):
path = "/get"
param.update(self.base_param)
self.rsp = requests.get(self.base_url + path, params=param)
self.ans = self.rsp.json()
logger.info(json.dumps(self.rsp.json(), indent=4))
return self
def set(self, param):
path = "/set"
param.update(self.base_param)
self.rsp = requests.get(self.base_url + path, params=param)
self.ans = self.rsp.json()
logger.info(json.dumps(self.rsp.json(), indent=4))
return self
def assert_statusCode(self, result_code):
"""
:param result_code: 包含關系斷言
:return: bool <= self.rsp.resultinfo
"""
# 回傳值,不符合預期時會設定為False
flag = True
if int(result_code) != self.rsp.status_code:
logger.error(f"回傳狀態碼[result_code]不符合預期:預期【{result_code}】!= 實際【{self.rsp.status_code}】")
flag = False
else:
logger.info(f"回傳狀態碼[result_code]符合預期:預期【{result_code}】!= 實際【{self.rsp.status_code}】")
if not self.continue_run_flag:
assert flag
return self
def assert_json_body(self, base, juge, contain=(), reg=()):
...
用例呼叫
# 如果僅僅斷言狀態碼
HttpBin().get({"name": "bingo", "age": 18}).assert_statusCode(200)
# 級連呼叫多個介面,使用同一個初始化資料
HttpBin().get({"name": "bingo", "age": 18}).assert_statusCode(200).\
set({"name": "he", "age": 18}).assert_statusCode(200).assert_json_body(
juge={
"args": {
"name": "bingo"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Cache-Control": "max-age=259200",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.27.1",
"X-Amzn-Trace-Id": r"Root=\d-\w{8}-\w{24}"
},
"req_param": [
{
"name": "he",
"age": "18"
}
],
"origin": r"",
"url": "http://httpbin.org/set?name=bingo"
}, contain=(), reg=("X-Amzn-Trace-Id", "origin",)
)
運行效果:
2022-05-05 19:39:36.951 | INFO | __main__:assert_statusCode:53 - 回傳狀態碼[result_code]符合預期:預期【200】!= 實際【200】
2022-05-05 19:39:36.951 | INFO | __main__:assert_json_body:117 - 欄位[name]符合預期: 預期[bingo]== 實際[bingo]
2022-05-05 19:39:36.951 | INFO | __main__:assert_json_body:117 - 欄位[args]符合預期: 預期[{'name': 'bingo'}]== 實際[{'name': 'bingo'}]
2022-05-05 19:39:36.952 | INFO | __main__:assert_json_body:117 - 欄位[Accept]符合預期: 預期[*/*]== 實際[*/*]
2022-05-05 19:39:36.952 | INFO | __main__:assert_json_body:117 - 欄位[Accept-Encoding]符合預期: 預期[gzip, deflate]== 實際[gzip, deflate]
2022-05-05 19:39:36.952 | INFO | __main__:assert_json_body:117 - 欄位[Cache-Control]符合預期: 預期[max-age=259200]== 實際[max-age=259200]
2022-05-05 19:39:36.952 | INFO | __main__:assert_json_body:117 - 欄位[Host]符合預期: 預期[httpbin.org]== 實際[httpbin.org]
2022-05-05 19:39:36.952 | INFO | __main__:assert_json_body:117 - 欄位[User-Agent]符合預期: 預期[python-requests/2.27.1]== 實際[python-requests/2.27.1]
2022-05-05 19:39:36.952 | INFO | __main__:assert_json_body:93 - 欄位[X-Amzn-Trace-Id]斷言成功:預期[Root=1-62734553-430db0707e1a3656043cd165]== 實際[Root=\d-\w{8}-\w{24}]
2022-05-05 19:39:36.952 | INFO | __main__:assert_json_body:117 - 欄位[headers]符合預期: 預期[{'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Cache-Control': 'max-age=259200', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.27.1', 'X-Amzn-Trace-Id': 'Root=1-62734553-430db0707e1a3656043cd165'}]== 實際[{'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Cache-Control': 'max-age=259200', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.27.1', 'X-Amzn-Trace-Id': 'Root=\\d-\\w{8}-\\w{24}'}]
2022-05-05 19:39:36.953 | WARNING | __main__:assert_json_body:77 - 欄位【origin】使用通用欄位的正則匹配, 符合預期
2022-05-05 19:39:36.953 | INFO | __main__:assert_json_body:117 - 欄位[age]符合預期: 預期[18]== 實際[18]
2022-05-05 19:39:36.953 | INFO | __main__:assert_json_body:117 - 欄位[name]符合預期: 預期[he]== 實際[he]
2022-05-05 19:39:36.953 | INFO | __main__:assert_json_body:117 - 欄位[req_param]符合預期: 預期[[{'age': '18', 'local_class': 'HttpBin', 'name': 'he'}]]== 實際[[{'name': 'he', 'age': '18'}]]
2022-05-05 19:39:36.953 | INFO | __main__:assert_json_body:117 - 欄位[url]符合預期: 預期[http://httpbin.org/set?name=bingo]== 實際[http://httpbin.org/set?name=bingo]
小結
- 可以作為獨立函式使用,也可以和所有的介面一起封裝,靈活度、復用度高
- 用例極簡且邏輯清晰
- 支持斷言失敗,用例繼續執行(開關控制),方便一次性發現所有的差異
- 極易撰寫的統一斷言(從日志獲取輕松獲取)
- 用例運行日志,斷言資料欄位清晰明確
- 可統一化處理新版本修改欄位,無需修改每個用例
- 支持正則匹配
- 支持**包含匹配 **
- 支持錯誤碼直接斷言
- 集中初始化原始常用資料,不同業務適配簡單,重寫初始化方法即可
進階
上面的方法雖然使用日志的方法記錄了所有的差異,但是面對大json對比的時候,很難直接標記出具體的差異位置,在做現網引流對比測驗的時候就出現了這樣的需求,從現網拉取的賬戶資料可能存在幾百個子賬戶,每個子賬戶有20多個屬性欄位,怎么準確標記他們在新舊系統的寫操作后不一致的情況成為了一個小卡點,
話不多說,思路:利用串列可變特性和生成器關鍵字yield特性遞回分解json,生成一個固定的陣列,最終比較陣列中的資料
代碼:
def recurse(d, prefix=None, sep='.'):
if prefix is None:
prefix = []
for key, value in d.items():
if isinstance(value, dict):
yield from recurse(value, prefix + [key])
elif isinstance(value, list):
for i, v in enumerate(value):
if isinstance(v, dict):
yield from recurse(v, prefix + [key, f"${i}"])
# 兼容 包含數字的型別
elif isinstance(v, int) or isinstance(v, str):
yield sep.join(prefix + [key, str(value)]) # 會嵌套上value
else:
# print(key)
yield sep.join(prefix + [key, str(value)]) # 會嵌套上value
效果:
print(json.dumps(list(recurse({
"args": {
"name": "bingo"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Cache-Control": "max-age=259200",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.27.1",
"X-Amzn-Trace-Id": r"Root=\d-\w{8}-\w{24}"
},
"req_param": [
{
"name": "bingo",
"age": "18"
},
{
"name": "he",
"age": "19"
},
{
"name": "detector",
"age": "20"
}
],
"origin": r"",
"url": "http://httpbin.org/set?name=bingo"
})), indent=4))
# 輸出
[
"args.name.bingo",
"headers.Accept.*/*",
"headers.Accept-Encoding.gzip, deflate",
"headers.Cache-Control.max-age=259200",
"headers.Host.httpbin.org",
"headers.User-Agent.python-requests/2.27.1",
"headers.X-Amzn-Trace-Id.Root=\\d-\\w{8}-\\w{24}",
"req_param.$0.name.bingo",
"req_param.$0.age.18",
"req_param.$1.name.he",
"req_param.$1.age.19",
"req_param.$2.name.detector",
"req_param.$2.age.20",
"origin.",
"url.http://httpbin.org/set?name=bingo"
]
總結
專案實踐中總會遇到這樣那樣的需求,每個方法都有適用的場景,直接高效解決問題是第一要務,
- json查找資料
- json資料用例封裝對比
- json資料轉化
合抱之木,生于毫末;九層之臺,起于累土;千里之行,始于足下,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/469832.html
標籤:其他
