Skip to main content

接口断言契约校验

Project description

接口断言引入契约校验

一.背景

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

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

目标:对返回数据进行全量(字段名-值/类型)校验


三.基本使用

示例:

from pactverify.matchers import Matcher, Like, EachLike, 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("王者荣耀")
    })
})

# 实际返回数据
actual_data = {
    "msg": "success",
    "code": 0,
    "data": [{
        # type_id类型不匹配
        "type_id": '249',
        "name": "王者荣耀"
    }, {
        # 缺少name
        "type_id": 250,
    }, {
        # 比契约定义多index字段,通过校验
        "type_id": 251,
        "name": "刺激战场",
        "index": 111
    }
    ]
}
mPactVerify = PactVerify(expect_format)
# 校验实际返回数据
mPactVerify.verify(actual_data)
# 校验结果  False
print(mPactVerify.verify_result)
''' 校验错误信息
错误信息输出actual_key路径:root.data.0.name形式
root为根目录,dict类型拼接key,list类型拼接数组下标(从0开始)
{
    # dict类型key不匹配错误
	'key_not_macth_error': ['root.data.1.name'],
	# 值不匹配错误
	'value_not_match_error': [],
	# 类型不匹配错误
	'type_not_match_error': [{
			'actual_key': 'root.data.0.type_id',
			'actual_vaule': '249',
			'expect_type': 'int'
		}
	],
	# 数组长度不匹配错误
	'list_len_not_match_error': [],
	# 元祖不匹配错误
	'enum_not_match_error': []
}

'''
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')
# 预期返回数据actual为dict结构,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')
# 预期返回数据actual为dict结构,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类

校验规则:枚举匹配

# 预期11或22
expected_format_1 = Enum([11, 22])
# iterate_list为true时,当目标数据为数组时,会遍历数组中每个元素是否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匹配

# nullable为true时允许返回null,预期null和(actual为dict结构,actual['k1'] == 'v1' or null)形式
expect_format = Matcher({'k1': 'v1'},nullable=True)
# nullable为true时允许返回null,预期null和(actual为dict结构,actual['k1'] == type('v1') or null)形式
expect_format = Like({'k1': 'v1'},nullable=True)
# nullable为true时允许返回null,预期null和[null,{'k1':null}]形式
expect_format = EachLike({'k1': 'v1'},nullable=True)
# nullable为true时允许返回null,预期null和11形式
expect_format = Term(r'^\d{2}$', example=11, nullable=True)
# nullable为true时允许返回null,预期null和11/22/33形式
expect_format = Enum([11, 22, 33], nullable=True)

5.2 {}匹配

# dict_emptiable为true时,允许返回{},预期{}和(actual为dict结构,actual['k1'] == 'v1')形式
expect_format = Matcher({'k1': 'v1'},dict_emptiable=True)
# dict_emptiable为true时,允许返回{},预期{}和(actual为dict结构,actual['k1'] == type('v1'))形式
expect_format = Like({'k1': 'v1'},dict_emptiable=True)

5.3 json格式字符串匹配

# actual为"{\"k1\":\"v1\"}"json字符串格式时,先进行json.dumps再校验
expect_format = Matcher({'k1':'v1'},jsonloads = True)
# actual为"{\"k1\":\"v1\"}"json字符串格式时,先进行json.dumps再校验
expect_format = Like({'k1': 'v1'},jsonloads = True)
# actual为"[{\"k1\":\"v1\"}]"json字符串格式时,先进行json.dumps再校验
expect_format = EachLike({'k1': 'v1'}, jsonloads = True)
# actual为"[11,22]"json字符串格式时,先进行json.dumps再校验
expected_format = Enum([11, 22],jsonloads = True)

5.4 key不存在匹配

# key_missable为true时,允许key不存在,key存在时走正常校验;Matcher,Like,EachLike,Term和Enum类都可使用该属性
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)
        })

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


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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pactverify-1.6.tar.gz (13.5 kB view hashes)

Uploaded Source

Built Distribution

pactverify-1.6-py3-none-any.whl (10.7 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page