接口测试 接口自动化全量字段校验

cool · 2020年03月17日 · 最后由 姜衍 回复于 2022年06月24日 · 12324 次阅读
本帖已被设为精华帖!

接口自动化全量字段校验

更新 2.0.1 版本:

  1. 新增 json 格式契约校验
  2. 新增根据响应结果自动生成 json 契约方法

github 地址:https://github.com/xglh/PactVerify_demo
与 httprunner(2.5.7) 结合 demo:https://github.com/xglh/httprunner-pactverify-demo


目录:

  • 一.背景
  • 二.校验原则
  • 三.快速使用
    • 1、python 类契约使用
    • 2、json 契约使用
    • 3、python 类契约转 json 契约
    • 4、根据响应结果自动生成 json 契约
  • 四.基本匹配规则
    • 1、Matcher 类,校验规则:值匹配
    • 2、Like 类,校验规则:类型匹配
    • 3、EachLike 类,校验规则:数组类型匹配
    • 4、Term 类,校验规则:正则匹配
    • 5、Enum 类,校验规则:枚举匹配
  • 五.复杂数据结构匹配规则
    • 1、{{}}格式
    • 2、[[]] 格式
    • 3、{[]}格式
    • 4、Like-Term 嵌套
    • 5、Like-Matcher 嵌套
  • 六. 异常场景匹配
    • 1、null 匹配
    • 2、{}匹配
    • 3、json 格式字符串匹配
    • 4、key 不存在匹配
    • 5、多类型匹配
    • 6、非强制字段匹配
  • 七.unittest+HTMLTestRunner+ 契约断言示例
  • 八.优点总结

一.背景

公司前端吐槽后台接口有时会更改返回的数据结构,返回的字段名与字段类型与接口文档不一致,希望有一个快速检测接口返回数据的所有字段名与字段类型的方法

以下方数据为例,要校验 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.字段值可以值相等或类型相等

目标:对返回数据进行全量 (字段名 - 值/类型) 校验
契约定义方式:支持 python 类契约和 json 契约


三.快速使用

安装:

pip install pactverify

1.python 类契约使用

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)

2.json 契约使用

from pactverify.matchers import PactJsonVerify

# 定义json契约格式
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': {
                # $values,$params形式传递额外参数
                '$values': 11,
                '$params': {
                    '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即可
# separator可自定义指定json关键字标识符,默认为$
mPactJsonVerify = PactJsonVerify(expect_format, hard_mode=True, separator='$')
# 校验实际返回数据
mPactJsonVerify.verify(actual_data)
# 校验结果  False
print(mPactJsonVerify.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(mPactJsonVerify.verify_info)

3.python 类契约转 json 契约

1python类契约不带参数
# python类契约
expect_format = Like({'k1': 'v1'})
# json契约
expect_format_json = {
    '$Like': {'k1': 'v1'}
}

2python类契约带参数
# python类契约
expect_format = Like({'k1': 'v1'}, nullable=True)
# json契约
expect_format_json = {
    '$Like': {
        # $values为契约类目标参数
        '$values': {'k1': 'v1'},
        # $params为额外参数的json数据
        '$params': {'nullable': True}
    }
}

4.根据响应结果自动生成 json 契约

from pactverify.utils import generate_pact_json_by_response

if __name__ == '__main__':
    response_json = {
        "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": "刺激战场"
        }
        ]
    }
    # 参数说明:响应json数据,契约关键字标识符(默认$)
    pact_json = generate_pact_json_by_response(response_json, separator='$')
    print(pact_json)
    '''
    # 模板生成只会包含$EachLike、$Like,可以根据具体校验需求更改,数组取第一个元素为模板来生成
    {
        '$Like': {
            'msg': 'success',
            'code': 0,
            'data': {
                '$EachLike': {
                    'type_id': 249,
                    'name': '王者荣耀',
                    'order_index': 1,
                    'status': 1,
                    'subtitle': ' ',
                    'game_name': '王者荣耀'
                }
            }
        }
    }
    '''
说明:
  1. PactVerify 与 PactJsonVerify 校验类只是契约数据不同,其他逻辑保持一致
  2. PactJsonVerify 关键字标志符可用 PactJsonVerify(separator='$') 自定义

四.基本匹配规则

1. Matcher 类

校验规则:值匹配

# 预期11  python类契约格式
expect_format_1 = Matcher(11)
# 预期11  json契约格式
expect_format_json_1 = {
    '$Matcher': 11
}

# 预期1.0 python类契约格式
expect_format_2 = Matcher(1.0)
# 预期1.0 json契约格式
expect_format_json_2 = {
    '$Matcher': 1.0
}

# 预期'11' python类契约格式
expect_format_3 = Matcher('11')
# 预期'11' json契约格式
expect_format_json_3 = {
    '$Matcher': '11'
}

# 预期返回数据actual为dict结构,actual['k1'] == 'v1'   python类契约格式
expect_format_4 = Matcher({'k1': 'v1'})
# 预期返回数据actual为dict结构,actual['k1'] == 'v1'   json契约格式
expect_format_json_4 = {
    '$Matcher': {'k1': 'v1'}
}

2. Like 类

校验规则:类型匹配

# 预期type(11)  python类契约
expect_format_1 = Like(11)
# 预期type(11)  json契约
expect_format_json_1 = {
    '$Like': 11
}

# 预期type(1.0)  python类契约
expect_format_2 = Like(1.0)
# 预期type(1.0)  json契约
expect_format_json_2 = {
    '$Like': 1.0
}


# 预期type('11')  python类契约
expect_format_3 = Like('11')
# 预期type('11')  json契约
expect_format_json_3 = {
    '$Like': '11'
}

# 预期返回数据actual为dict结构,actual['k1'] == type('v1')   python类契约
expect_format_4 = Like({'k1':'v1'})
# 预期返回数据actual为dict结构,actual['k1'] == type('v1')   json契约
expect_format_json_4 =  {
    '$Like': {'k1':'v1'}
}

3. EachLike 类

校验规则:数组类型匹配

# 预期[type(11)]  python类契约
expect_format_1 = EachLike(11)
# 预期[type(11)]  json契约
expect_format_json_1 = {
    '$EachLike': 11
}

# 预期[type(1.0)]  python类契约
expect_format_2 = EachLike(1.0)
# 预期[type(1.0)]  json契约
expect_format_json_2 = {
    '$EachLike': 1.0
}

# 预期[type('11')]  python类契约
expect_format_3 = EachLike('11')
# 预期[type('11')]  json契约
expect_format_json_3 = {
    '$EachLike': '11'
}

# 预期[Like{'k1':'v1'}]  python类契约
expect_format_4 = EachLike({'k1': 'v1'})
# 预期[Like{'k1':'v1'}]  json契约
expect_format_json_4 = {
    '$EachLike': {'k1': 'v1'}
}

# 预期[Like{'k1':'v1'}]或[],minimum为数组最小长度,默认minimum=1   python类契约
expect_format_5 = EachLike({'k1': 'v1'}, minimum=0)
# 预期[Like{'k1':'v1'}]或[],minimum为数组最小长度,默认minimum=1   json契约
expect_format_json_5 = {
    '$EachLike': {
        # $values,$params结构用于额外传参
        '$values': {'k1': 'v1'},
        '$params': {'minimum': 0}
    }
}

4. Term 类

校验规则:正则匹配

# 预期r'^\d{2}$',并且type(actual_data) == type(example),example也用来测试正则表达式  python类契约
expect_format_1 = Term(r'^\d{2}$', example=11)
# 预期r'^\d{2}$',并且type(actual_data) == type(example),example也用来测试正则表达式  json契约
expect_format_json_1 = {
    '$Term': {
        '$values': r'^\d{2}$',
        '$params': {'example': 11}
    }
}

# 预期r'^\d{2}$',example用来测试正则表达式,type_strict = False时跳过对example参数类型校验   python类契约
expect_format_2 = Term(r'^\d{2}$', example="11", type_strict=False)
# 预期r'^\d{2}$',example用来测试正则表达式,type_strict = False时跳过对example参数类型校验   json契约
expect_format_json_2 = {
    '$Term': {
        '$values': r'^\d{2}$',
        '$params': {'example': 11, 'type_strict': False}
    }
}

5. Enum 类

校验规则:枚举匹配

# 预期11或22  python类契约
expected_format_1 = Enum([11, 22])
# 预期11或22  json契约
expected_format_json_1 = {
    '$Enum': [11, 22]
}

# iterate_list为true时,当目标数据为数组时,会遍历数组中每个元素是否in [11, 22]  python类契约
expected_format_2 = Enum([11, 22], iterate_list=True)
# iterate_list为true时,当目标数据为数组时,会遍历数组中每个元素是否in [11, 22]  json契约
expected_format_json_2 = {
    '$Enum': {
        '$values': [11, 22],
        '$params': {'iterate_list': True}
    }
}

五.复杂规则匹配

1.{{}}格式

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

# python类契约
expect_format = Like({
    'code': 0,
    'msg': 'success',
    'data': Like({
        "id": 1,
        "name": 'lili'
    })
})

# json契约
expect_format_json = {
    '$Like': {
        'code': 0,
        'msg': 'success',
        'data': {
            '$Like': {
                "id": 1,
                "name": 'lili'
            }
        }
    }
}

2.[[]] 格式

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

# python类契约
expect_format = EachLike(EachLike({
    "id": 1,
    "name": 'lili'
}))

# json契约
expect_format_json = {
    '$EachLike': {
        '$EachLike': {
            "id": 1,
            "name": 'lili'
        }
    }
}

3.{[]}格式

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

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

# json契约
expect_format_json = {
    '$Like': {
        'code': 0,
        'msg': 'success',
        'data': {
            '$EachLike': {
                "id": 1,
                "name": 'lili'
            }
        }
    }
}

4.Like-Term 嵌套

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

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

# json契约
expect_format_json = {
    '$Like': {
        'code': 0,
        'msg': 'success',
        'data': {
            '$Like': {
                "id": 1,
                "name": {
                    '$Term': {
                        '$values': r'\w*',
                        '$params': {
                            'example': 'lili'
                        }
                    }
                }
            }
        }
    }
}

5.Like-Matcher 嵌套

actual_data = {
    'name': 'lilei',
    'age': 12
}

# python类契约
expect_format = Like({
    # name字段值类型匹配
    'name': 'lilei',
    # age字段值匹配
    'age': Matcher(12),
})

# json契约
expect_format_json = {
    '$Like': {
        # name字段值类型匹配
        'name': 'lilei',
        # age字段值匹配
        'age': {
            '$Matcher': 12
        },
    }
}

说明:

  1. Matcher,Like 和 EachLike 类可以不限层级嵌套,Term 和 Enum 则不能嵌套其他规则
  2. 匹配规则多层嵌套时,内层规则优先生效

六.异常场景匹配

1.null 匹配

# nullable为true时允许返回null,预期null和(actual为dict结构,actual['k1'] == 'v1' or null)形式   python类契约
expect_format = Matcher({'k1': 'v1'}, nullable=True)
# nullable为true时允许返回null,预期null和(actual为dict结构,actual['k1'] == 'v1' or null)形式   json契约
expect_format_json = {
    '$Matcher': {
        '$values': {'k1': 'v1'},
        '$params': {'nullable': True}
    }
}

# nullable为true时允许返回null,预期null和(actual为dict结构,actual['k1'] == type('v1') or null)形式   python类契约
expect_format = Like({'k1': 'v1'}, nullable=True)
# nullable为true时允许返回null,预期null和(actual为dict结构,actual['k1'] == type('v1') or null)形式   json契约
expect_format_json = {
    '$Like': {
        '$values': {'k1': 'v1'},
        '$params': {'nullable': True}
    }
}

# nullable为true时允许返回null,预期null和[null,{'k1':null}]形式   python类契约
expect_format = EachLike({'k1': 'v1'}, nullable=True)
# nullable为true时允许返回null,预期null和[null,{'k1':null}]形式   json契约
expect_format_json = {
    '$EachLike': {
        '$values': {'k1': 'v1'},
        '$params': {'nullable': True}
    }
}

# nullable为true时允许返回null,预期null和11形式   python类契约
expect_format = Term(r'^\d{2}$', example=11, nullable=True)
# nullable为true时允许返回null,预期null和11形式   json契约
expect_format_json = {
    '$Term': {
        '$values': r'^\d{2}$',
        '$params': {'example': 11, 'nullable': True}
    }
}

# nullable为true时允许返回null,预期null和11/22/33形式   python类契约
expect_format = Enum([11, 22, 33], nullable=True)
# nullable为true时允许返回null,预期null和11/22/33形式   json契约
expect_format_json = {
    '$Enum': {
        '$values': [11, 22, 33],
        '$params': {'nullable': True}
    }
}

备注:nullable 参数在 hard_mode = True 时也生效

2.{}匹配

# dict_emptiable为true时,允许返回{},预期{}和(actual为dict结构,actual['k1'] == 'v1')形式   python类契约
expect_format = Matcher({'k1': 'v1'}, dict_emptiable=True)
# dict_emptiable为true时,允许返回{},预期{}和(actual为dict结构,actual['k1'] == 'v1')形式   json契约
expect_format_json = {
    '$Matcher': {
        '$values': {'k1': 'v1'},
        '$params': {'dict_emptiable': True}
    }
}

# dict_emptiable为true时,允许返回{},预期{}和(actual为dict结构,actual['k1'] == type('v1'))形式   python类契约
expect_format = Like({'k1': 'v1'}, dict_emptiable=True)
# dict_emptiable为true时,允许返回{},预期{}和(actual为dict结构,actual['k1'] == type('v1'))形式   json契约
expect_format_json = {
    '$Like': {
        '$values': {'k1': 'v1'},
        '$params': {'dict_emptiable': True}
    }
}

备注:dict_emptiable 在 hard_mode = True 时也生效

3.json 格式字符串匹配

# actual为"{\"k1\":\"v1\"}"json字符串格式时,先进行json.loads再校验   python类契约
expect_format = Matcher({'k1': 'v1'}, jsonloads=True)
# actual为"{\"k1\":\"v1\"}"json字符串格式时,先进行json.loads再校验   json契约
expect_format_json = {
    '$Matcher': {
        '$values': {'k1': 'v1'},
        '$params': {'jsonloads': True}
    }
}

# actual为"{\"k1\":\"v1\"}"json字符串格式时,先进行json.loads再校验   python类契约
expect_format = Like({'k1': 'v1'}, jsonloads=True)
# actual为"{\"k1\":\"v1\"}"json字符串格式时,先进行json.loads再校验   json契约
expect_format_json = {
    '$Like': {
        '$values': {'k1': 'v1'},
        '$params': {'jsonloads': True}
    }
}

# actual为"[{\"k1\":\"v1\"}]"json字符串格式时,先进行json.loads再校验  python类契约
expect_format = EachLike({'k1': 'v1'}, jsonloads=True)
# actual为"[{\"k1\":\"v1\"}]"json字符串格式时,先进行json.loads再校验  json契约
expect_format_json = {
    '$EachLike': {
        '$values': {'k1': 'v1'},
        '$params': {'jsonloads': True}
    }
}

# actual为"[11,22]"json字符串格式时,先进行json.loads再校验   python类契约
expected_format = Enum([11, 22], jsonloads=True)
# actual为"[11,22]"json字符串格式时,先进行json.loads再校验   json契约
expected_format_json = {
    '$Enum': {
        '$values': {'k1': 'v1'},
        '$params': {'jsonloads': True}
    }
}

4.key 不存在匹配

# key_missable为true时,允许key不存在,key存在时走正常校验;Matcher,Like,EachLike,Term和Enum类都可使用该属性   python类契约
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为true时,允许key不存在,key存在时走正常校验;Matcher,Like,EachLike,Term和Enum类都可使用该属性   json契约
expect_format_json = {
    '$Matcher': {
        'code': {
            '$Like': {
                '$values': 0,
                '$params': {'key_missable': True}
            }
        },
        'msg': {
            '$Matcher': {
                '$values': 'success',
                '$params': {'key_missable': True}
            }
        },
        'data': {
            '$EachLike': {
                '$values': 11,
                '$params': {'key_missable': True}
            }
        },
        'age': {
            '$Term': {
                '$values': r'^\d{2}$',
                '$params': {'example': 11, 'key_missable': True}
            }
        },
        'num': {
            '$Enum': {
                '$values': [11, 22, 33],
                '$params': {'key_missable': True}
            }
        },
    }}

# dict_key_missable为true时,允许dict结构中的key不存在,但key不能多(hard_mode=true时),key存在时正常校验  python类契约
expected_format = Matcher({
    'name': 'lilei',
    'age': 12,
    'sex': 'man'
}, dict_key_missable=True)
# dict_key_missable为true时,允许dict结构中的key不存在,但key不能多(hard_mode=true时),key存在时正常校验  json契约
expected_format_json = {
    '$Matcher': {
        '$values': {
            'name': 'lilei',
            'age': 12,
            'sex': 'man'
        },
        '$params': {'dict_key_missable': True}
    }
}

# dict_key_missable为true时,允许dict结构中的key不存在,但key不能多(hard_mode=true时),key存在时正常校验   python类契约
expected_format = Like({
    'name': 'lilei',
    'age': 12,
    'sex': 'man'
}, dict_key_missable=True)
# dict_key_missable为true时,允许dict结构中的key不存在,但key不能多(hard_mode=true时),key存在时正常校验   json契约
expected_format_json = {
    '$Like': {
        '$values': {
            'name': 'lilei',
            'age': 12,
            'sex': 'man'
        },
        '$params': {'dict_key_missable': True}
    }
}

# dict_key_missable为true时,允许dict结构中的key不存在,但key不能多(hard_mode=true时),key存在时正常校验   python类契约
expected_format = EachLike({
    'name': 'lilei',
    'age': 12,
    'sex': 'man'
}, dict_key_missable=True)
# dict_key_missable为true时,允许dict结构中的key不存在,但key不能多(hard_mode=true时),key存在时正常校验   json契约
expected_format_json = {
    '$EachLike': {
        '$values': {
            'name': 'lilei',
            'age': 12,
            'sex': 'man'
        },
        '$params': {'dict_key_missable': True}
    }
}

5.多类型匹配

# actual数据为type(11)或type('11'),extra_types可以添加多个示例数据,对基础数据类型(int,float,boolean,str,None)示例有效,对list dict等类型无效  python类契约
expect_format = Like(11, extra_types=['11'])
# actual数据为type(11)或type('11'),extra_types可以添加多个示例数据,对基础数据类型(int,float,boolean,str,None)示例有效,对list dict等类型无效  json契约
expect_format_json = {
    '$Like': {
        '$values': 11,
        '$params': {'extra_types': ['11']}
    }
}

# actual数据为[type(11)]或[type('11')],extra_types可以添加多个示例数据,对基础数据类型示例(int,float,boolean,str,None)有效,对list dict等类型无效  python类契约
expect_format = EachLike(11, extra_types=['11'])
# actual数据为[type(11)]或[type('11')],extra_types可以添加多个示例数据,对基础数据类型示例(int,float,boolean,str,None)有效,对list dict等类型无效  json契约
expect_format_json = {
    '$EachLike': {
        '$values': 11,
        '$params': {'extra_types': ['11']}
    }
}

6.非强制字段匹配

expect_format = Like({'k1': 'v1'})
# hard_mode=False只匹配契约中定义的字段,实际返回的多余字段忽略    python类契约
mPactVerify = PactVerify(expect_format, hard_mode=False)
actual_data = {'k1': 'v1', 'k2': 'v2'}
# 只校验k1字段,k2字段忽略
mPactVerify.verify(actual_data)

expect_format_json = {
    '$Like': {'k1': 'v1'}
}
# hard_mode=False只匹配契约中定义的字段,实际返回的多余字段忽略   json契约
mPactJsonVerify = PactJsonVerify(expect_format, hard_mode=False)
actual_data = {'k1': 'v1', 'k2': 'v2'}
# 只校验k1字段,k2字段忽略
mPactJsonVerify.verify(actual_data)

备注:
1. key_missable 在 hard_mode = True 时也生效
2. key_missable 针对 actual_data 本身的 key,dict_key_missable 针对 actual_data 字典中的 key,可以同时生效

注意:异常匹配场景越多,代表接口数据格式越不规范


七.配合 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.可复用接口实际响应数据来定义契约

共收到 53 条回复 时间 点赞

第一次看到校验全字段的哦

simple 回复

大佬,现在 360 火线还在更新维护么?支持 Jenkins pipeline 了么

cool #4 · 2020年03月18日 Author
simple 回复

jsonschema 写起来太过复杂,完全不想用😂

验证复杂嵌套接口适用性咋样,看着比 jsonschema 精简点

cool #6 · 2020年03月18日 Author
aibreze 回复

这个支持无限层级的嵌套,正常场景和异常场景都能满足,我们自己近 2K 的自动化用例都接入了,已经快半年没改动了,有兴趣的话可以试试

韩将 回复

我们正在弄流水线,今年准备把之前的东西都集成到流水线里作为插件,火线也是,会支持 Jenkins pipeline 脚本

有时候会遇到在有些 case 中,同一个 API 相同的请求在不同场景中返回的 response 结构不同但也是合理的情况,特别是 GET 例如保存个人信息前返回数据为空,保存了姓名之后 GET 返回姓名新,再次保存年龄后返回年龄 + 姓名,请教下在这种情况下,是怎么处理的呢

run_ice_l 回复

这个有点不合理吧,前端的同学不是很苦逼?

很棒的感觉,不知道能否继承到 HttpRunner 中?

cool #11 · 2020年03月19日 Author
run_ice_l 回复

1、你们这个接口返回结构是有问题,没值的字段也要返回出来,这个最好给后台提 bug 处理
2、"怎么处理"的意思是想用上边这个模块来断言么?

cool #12 · 2020年03月19日 Author
HazeMaker 回复

这个是可以的,我最早的一版就是跟 HttpRunner 配合使用的,后边单独抽离出来了

每个接口返回不一样 如何达到通用呢?

cool #14 · 2020年03月19日 Author
liuweiwei66 回复

针对每个接口需要定义预期的数据格式,按照{}或者 [] 结构来组织数据,能够相互嵌套,能定义成任何结构的数据

cool 回复

你是每个接口定义一套 还是定义一套适配所有接口

cool #16 · 2020年03月19日 Author
liuweiwei66 回复

每个接口定义一套,"通用"指的是后边的对比逻辑处理

cool 回复

那岂不是很麻烦 每个接口都要定义一套契约
不过对于全字段处理是个好办法

cool #18 · 2020年03月19日 Author
liuweiwei66 回复

字段多了肯定不能手动填的,可以复用实际的返回结果,把对应的结构规则改下就行

大佬牛 p,666

我喜欢直接 diff😀

simple 回复

jsonschema 有个问题,就是 validate 的时候,如果校验到第一个不匹配的项就抛异常了,程序终止了,导致后面的项无法在当次完成校验。这个问题,有什么解决方法吗,想让它全部校验完毕,列举出不匹配的项

这个好,这个好,我喜欢这个😀

这个好,有时间试试

匿名 #24 · 2020年04月30日

可以把一个 schema 拆成几个小的 schema。程序停止是因为写的语法错误。

25楼 已删除

今天试了一下,好像没办法做到用例和代码分离。
响应模板只能写在代码里,如果写在 DB 里,读出来之后是字符串,执行会报错
甚至不能将字符串转换为字典,,
楼主有什么解决办法么

cool #27 · 2020年05月13日 Author
张哈哈 回复

暂时没办法把这部分数据放到 DB 里面,但是有个方法可行:单独定义一个 class 存放校验格式属性 a,通过 getattr(a) 来获取,在 DB 里面存这个属性 a 的名称就行

cool [92年 大龄阿姨的求助帖] 接口用例维护 中提及了此贴 05月15日 17:42

之前用 jmeter 的时候,也写过,对接口返回的字段以及值进行校验,我的思路是通过 json 路径匹配,第二个就是字段的值也可以校验格式(配置哪个字段什么类型),还支持一个接口支持多个结构匹配;对比的模板直接拿线上的,类似 diff功能,简单一些!

cool #30 · 2020年05月20日 Author
花落去 回复

目的达到了就可以,但我这个要给其他人使用,易用性,场景覆盖都需要去考虑

最近也有做这个,我的做法是模仿 mongoengine 的 field 定义不同的 field,再组织成一个 document

--“前端吐槽后台接口有时会更改返回的数据结构,返回的字段名与字段类型与接口文档不一致”。
前端和后台必须遵循共同/相同的接口文档。
如果后台按照接口文档更改返回的数据结构,则前端也需要按照接口文档去使用。
如果后台更改返回的数据结构不是按照接口文档来的,前端在使用的过程中一经发现,报告后台的这种不规范行为,要求改正即可。
通过管理手段可以轻松解决的问题,通过技术手段解决起来要麻烦好多。

cool 回复

如果接口设计文档规定的返回结构是没值的字段不允许返回,这就不是 bug,是 by design。

cool #34 · 2020年07月17日 Author
Thirty-Thirty 回复

你想的太理想了,如果接口文档能实时更新,前后端开发完全按照文档来,那这个自然没问题,但是实际项目中很难做到这点,至少我经历的公司做不到这种程度;管理终究是人来做的,是人都会有疏忽和懈怠,有技术手段去做一个补充的话我觉得更好

cool #35 · 2020年07月17日 Author
Thirty-Thirty 回复

我感觉你是开发,站在测试的角度,如果是老接口,我可以接受没值的字段不返回的情况;如果是新开发的接口,我会当 bug 处理

大佬的业务痛点我也遇到过,公司人多,业务方调用项目的接口,项目的接口改动,没有第一时间告知调用方,导致接口上线后,接口变更影响到了调用方线上业务功能。
最近也在想怎么搞契约测试?如何第一时间知道接口的改动评估是否影响调用方。看网上有些是消费者生成消费者契约,让提供者去按契约进行 mock 调用。

cool #37 · 2020年07月29日 Author
wuwei 回复

我不是大佬😂 ;契约测试我也研究过,也搞过一个 demo 出来,最终落地不了了之,网上也没啥比较成熟方案;这个比接口测试的层级更底一点,属于测试左移的范畴,越底层的话,写脚本需要投入的时间就越多,我觉得从接口层搞起来比较合适,先把接口层的覆盖的差不多了再考虑往下层考虑。

嗯嗯是的,我最近就在研究接口自动化,pytest+requests+allure,还没有涉及到平台化,在写框架时突然看到你的这个,我就应用在项目中了,在接口返回后先做底层校验,就是引用你的这个技术,然后再进行接口自动化的校验,嘿嘿,还得管用的。
但是有个比较麻烦的跟契约测试一样,要全量校验,接口返回字段多的话,就比较麻烦了,需要一个一个字段定义契约。。。

cool #39 · 2020年07月29日 Author
wuwei 回复

这个不用一个个字段写啊,一般把实际返回的数据复制进去再改改就好了

请教楼主,对于{"a":"1","b":"2","c":"3","d":"4","e":"5","f":"6","g":"7",....}这种返回数据是非固定长度的,键也是不固定的,类型固定都是 str,要怎么校验呢?我用了 EachLike 不行啊!

cool #41 · 2020年08月14日 Author
skysleepy 回复

值匹配,针对字典{"a": "1","b": "1"}类型

expect_format = Matcher({
"a": "1", # a key 存在,值为"1",
"b": "2", # b key 存在,值为"2",
})

值类型匹配,针对字典{"a": "1","b": "1"}类型

expect_format_2 = Like({
"a": "1", # a key 存在,值类型为 type("1")
"b": "2", # b key 存在,值类型为 type("2")
})

数组值类型匹配,针对数组 - 字典 [{"a": "1","b": "1"},{"a": "1","b": "1"}] 类型

expect_format_3 = EachLike({
"a": "1", # a key 存在,值类型为 type("1")
"b": "2", # b key 存在,值类型为 type("2")
})

看是要做值校验,还是值类型校验

cool 回复

请问是如何结合 httprunner 进行全量字段校验的,可以提供源码例子吗?

cool #43 · 2020年09月05日 Author
loneyao 回复

写了个基于 httprunner 2.5.7 版本的 demo,https://github.com/xglh/httprunner-pactverify-demo, httprunner 3.0 版本的实在短时间内没搞懂😂

cool 接口测试返回结构对比实现思路记录 中提及了此贴 09月05日 20:37
cool 回复

非常赞!

simple 回复

请教大佬,你们的 jsonschema 校验的模板,是手动去http://JSONschema.net/#/home生成的,还是自己写的代码呢?

爱吃米饭 回复

自动检验的,模版是提前配置好的

cool #48 · 2020年11月10日 Author
张哈哈 回复

新版本支持 json 格式校验

simple 回复

您好,请教下 jsonschema 模板如果手动去http://JSONschema.net/#/home生成会不会太麻烦啦?

在路上 服务端接口测试指南 中提及了此贴 05月08日 17:28
恒温 将本帖设为了精华贴 06月03日 06:36

java 我们上 jsonunit

54楼 已删除

你好,想问一下如果做值匹配的话,契约里面要怎么通过变量定义呢?

cool #56 · 2021年06月24日 Author
404铁头娃 回复

没明白你的意思,能举个例子么?

仅楼主可见
cool · #58 · 2021年06月25日 Author
仅楼主可见

我想知道 ,怎么根据契约生成 mock 数据呢

兔子🐰 [该话题已被删除] 中提及了此贴 02月18日 16:23
simple 回复

我们是 schema

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册