介面自動化全量欄位校驗

小酷發表於2020-03-17

介面自動化全量欄位校驗

一.背景

公司前端吐槽後臺介面有時會更改返回的資料結構,返回的欄位名與欄位型別與介面文件不一致,希望有一個快速檢測介面返回資料的所有欄位名與欄位型別的方法

以下方資料為例,要校驗data陣列中dict結構中的欄位名與欄位型別,可以寫指令碼遍歷資料,但是由於每個介面返回的資料結構可能不一致,可能需要針對每個介面做不同的邏輯,所以需要一個比較通用的校驗方法

{
"msg": "success",
"code": 0,
"data": [{
"type_id": 249,
"name": "王者榮耀",
"order_index": 1,
"status": 1,
"subtitle": " ",
"game_name": "王者榮耀"
}, {
"type_id": 250,
"name": "絕地求生",
"order_index": 2,
"status": 1,
"subtitle": " ",
"game_name": "絕地求生"
}, {
"type_id": 251,
"name": "刺激戰場",
"order_index": 3,
"status": 1,
"subtitle": " ",
"game_name": "刺激戰場"
}
]
}

在研究了契約測試後,抽取pact-python部分程式碼,實現:自定義介面返回資料格式(【契約定義】)-實際響應資料格式校驗(【契約校驗】)的功能

備註:這裡的【契約】等同於介面響應資料結構


二.校驗原則

1.實際返回欄位名要嚴格等於或者含契約定義欄位名(根據不同匹配模式來確定)
2.欄位值可以值相等或型別相等

目標:對返回資料進行全量(欄位名-值/型別) 校驗


三.基本使用

安裝:

pip install pactverify

示例:

from pactverify.matchers import Matcher, Like, EachLike, Enum, Term, PactVerify

# 定義契約格式
expect_format = Matcher({
'code': 0, # code key存在,值相等,code==0
'msg': 'success', # msg key存在,值相等,msg=='success'
# [{}]結構
'data': EachLike({
"type_id": 249, # type_id key存在,值型別相等,type(type_id) == type(249)
"name": "王者榮耀", # name key存在,值型別相等,type(name) == type("王者榮耀")
}),
'type': Enum([11,22]),
'list': EachLike(11,minimum=2)
})

# 實際返回資料
actual_data = {
"msg": "success",
"code": 1,
'type': 12,
"data": [{
# type_id型別不匹配
"type_id": '249',
"name": "王者榮耀"
}, {
# 缺少name
"type_id": 250,
}, {
# 比契約定義多index欄位
"type_id": 251,
"name": "刺激戰場",
"index": 111
}
],
'list': [11]
}
# hard_mode預設為true,hard_mode = True,實際返回key必須嚴格等於預期key;hard_mode = False,實際返回key包含預期key即可
mPactVerify = PactVerify(expect_format, hard_mode=True)
# 校驗實際返回資料
mPactVerify.verify(actual_data)
# 校驗結果 False
print(mPactVerify.verify_result)
''' 校驗錯誤資訊
錯誤資訊輸出actual_key路徑:root.data.0.name形式
root為根目錄,dict型別拼接key,list型別拼接陣列下標(0開始)
{
# 實際key少於預期key錯誤
'key_less_than_expect_error': ['root.data.1.name'],
# 實際key多與預期key錯誤,只在hard_mode = True時才報該錯誤
'key_more_than_expect_error': ['root.data.2.index'],
# 值不匹配錯誤
'value_not_match_error': [{
'actual_key': 'root.code',
'actual_value': 1,
'expect_value': 0
}
],
# 型別不匹配錯誤
'type_not_match_error': [{
'actual_key': 'root.data.0.type_id',
'actual_vaule': '249',
'expect_type': 'int'
}
],
# 陣列長度不匹配錯誤
'list_len_not_match_error': [{
'actual_key': 'root.list',
'actual_len': 1,
'min_len': 2
}
],
# 列舉不匹配錯誤
'enum_not_match_error': [{
'actual_key': 'root.type',
'actual_value': 12,
'expect_enum': [11, 22]
}
]
}

'''
print(mPactVerify.verify_info)

1. Matcher類

校驗規則:值匹配

# 預期11
expect_format_1 = Matcher(11)
# 預期1.0
expect_format_2 = Matcher(1.0)
# 預期'11'
expect_format_3 = Matcher('11')
# 預期返回資料actualdict結構,actual['k1'] == 'v1'
expect_format_4 = Matcher({'k1':'v1'})

2. Like類

校驗規則:型別匹配

# 預期type(11)
expect_format_1 = Like(11)
# 預期type(1.0)
expect_format_2 = Like(1.0)
# 預期type('11')
expect_format_3 = Like('11')
# 預期返回資料actualdict結構,actual['k1'] == type('v1')
expect_format_4 = Like({'k1':'v1'})

3. EachLike類

校驗規則:陣列型別匹配

# 預期[type(11)]
expect_format_1 = EachLike(11)
# 預期[type(1.0)]
expect_format_2 = EachLike(1.0)
# 預期[type('11')]
expect_format_3 = EachLike('11')
# 預期[Like{'k1':'v1'}]
expect_format_4 = EachLike({'k1': 'v1'})
# 預期[Like{'k1':'v1'}][],minimum為陣列最小長度,預設minimum=1
expect_format_4 = EachLike({'k1': 'v1'}, minimum=0)

4. Term類

校驗規則:正則匹配

# 預期r'^\d{2}$',並且type(actual_data) == type(example)example也來測試正規表示式
expect_format_1 = Term(r'^\d{2}$', example=111)

5. Enum類

校驗規則:列舉匹配

# 預期1122
expected_format_1 = Enum([11, 22])
# iterate_listtrue時,當目標資料為陣列時,會遍歷陣列中每個元素是否in [11, 22]
expected_format_2 = Enum([11, 22],iterate_list=True)

四.複雜規則匹配

4.1 {{}}格式

actual_data = {
'code': 0,
'msg': 'success',
'data': {
"id": 1,
"name": 'lili'
}
}
expect_format = Like({
'code': 0,
'msg': 'success',
'data': Like({
"id": 1,
"name": 'lili'
})
})

4.2 [[]]格式

actual_data = [[{
"id": 1,
"name": 'lili'
}]]

expect_format = EachLike(EachLike({
"id": 1,
"name": 'lili'
}))

4.3 {[]}格式

actual_data = {
'code': 0,
'msg': 'success',
'data': [{
"id": 1,
"name": 'lili'
},{
"id": 2,
"name": 'lilei'
}]
}

expect_format = Like({
'code': 0,
'msg': 'success',
'data': EachLike({
"id": 1,
"name": 'lili'
})
})

4.4 Like-Term巢狀

expect_format = Like({
'code': 0,
'msg': 'success',
'data': Like({
"id": 1,
"name": Term(r'\w*',example='lili')
})
})

4.5 Like-Matcher巢狀

expect_format = Like({
# name欄位值型別匹配
'name': 'lilei',
# age欄位值匹配
'age': Matcher(12),
})

說明:

  1. Matcher,Like和EachLike類可以不限層級巢狀,Term和Enum則不能巢狀其他規則
  2. 匹配規則多層巢狀時,內層規則優先生效

五.異常場景匹配

5.1 null匹配

# nullabletrue時允許返回null,預期null和(actualdict結構,actual['k1'] == 'v1' or null)形式
expect_format = Matcher({'k1': 'v1'},nullable=True)
# nullabletrue時允許返回null,預期null和(actualdict結構,actual['k1'] == type('v1') or null)形式
expect_format = Like({'k1': 'v1'},nullable=True)
# nullabletrue時允許返回null,預期null[null,{'k1':null}]形式
expect_format = EachLike({'k1': 'v1'},nullable=True)
# nullabletrue時允許返回null,預期null11形式
expect_format = Term(r'^\d{2}$', example=11, nullable=True)
# nullabletrue時允許返回null,預期null11/22/33形式
expect_format = Enum([11, 22, 33], nullable=True)

備註:nullable引數在hard_mode = True時也生效

5.2 {}匹配

# dict_emptiabletrue時,允許返回{},預期{}和(actualdict結構,actual['k1'] == 'v1')形式
expect_format = Matcher({'k1': 'v1'},dict_emptiable=True)
# dict_emptiabletrue時,允許返回{},預期{}和(actualdict結構,actual['k1'] == type('v1'))形式
expect_format = Like({'k1': 'v1'},dict_emptiable=True)

備註:dict_emptiable在hard_mode = True時也生效

5.3 json格式字串匹配

# actual"{\"k1\":\"v1\"}"json字串格式時,先進行json.loads再校驗
expect_format = Matcher({'k1':'v1'},jsonloads = True)
# actual"{\"k1\":\"v1\"}"json字串格式時,先進行json.loads再校驗
expect_format = Like({'k1': 'v1'},jsonloads = True)
# actual"[{\"k1\":\"v1\"}]"json字串格式時,先進行json.loads再校驗
expect_format = EachLike({'k1': 'v1'}, jsonloads = True)
# actual"[11,22]"json字串格式時,先進行json.loads再校驗
expected_format = Enum([11, 22],jsonloads = True)

### 5.4 key不存在匹配
```python
# key_missabletrue時,允許key不存在,key存在時走正常校驗;Matcher,Like,EachLike,TermEnum類都可使用該屬性
expect_format = Matcher({
'code': Like(0, key_missable=True),
'msg': Matcher('success', key_missable=True),
'data': EachLike(11, key_missable=True),
'age': Term(r'^\d{2}$', example=11, key_missable=True),
'num': Enum([11, 22, 33], key_missable=True)
})

備註:key_missable在hard_mode = True時也生效

注意:異常匹配場景越多,代表介面資料格式越不規範


六.配合unittest+requests使用

import unittest, requests, HtmlTestRunner, os
from pactverify.matchers import Matcher, Like, EachLike, Term, Enum, PactVerify


class PactTest(unittest.TestCase):

def test_config_2(self):
url = 'http://127.0.0.1:8080/configV2'
config_rsp = requests.get(url)
config_contract_format = Matcher({
"msg": "success",
"code": 200,
'name': Enum(['lili', 'xiaohei']),
'addr': Term(r'深圳*', example='深圳寶安'),
"data": EachLike({
"type_id": 249,
"name": "王者榮耀",
"order_index": 1,
"status": 1,
"subtitle": " ",
"game_name": "王者榮耀"
}),
'data_2':
EachLike({
"type_id": 249,
"name": "王者榮耀",
"order_index": 1,
"status": 1,
"subtitle": " ",
"game_name": "王者榮耀"
}, minimum=1)
})

mPactVerify = PactVerify(config_contract_format)

try:
actual_rsp_json = config_rsp.json()
mPactVerify.verify(actual_rsp_json)
assert mPactVerify.verify_result == True
except Exception:
# 自定義錯誤資訊,輸出到HTMLTestRunner
err_msg = 'PactVerify_fail,verify_result:{},verify_info:{}'.format(mPactVerify.verify_result,
mPactVerify.verify_info)
self.fail(err_msg)


if __name__ == '__main__':
current_path = os.path.abspath(__file__)
current_dir = os.path.abspath(os.path.dirname(current_path) + os.path.sep + ".")
suite = unittest.defaultTestLoader.discover(current_dir, pattern="test_*.py")
runner = HtmlTestRunner.HTMLTestRunner(combine_reports=True, report_name="MyReport", add_timestamp=False)
runner.run(suite)

七.優點總結

1.顯式定義介面斷言格式,介面斷言更加直觀
2.可複用介面實際響應資料來定義契約

github地址:https://github.com/xglh/PactVerify_demo

相關文章