契约测试笔记
字面上意思是签订一份双方甚至多方的的一份约定/合同,最早可以出现在买卖,抵押等活动,如果一方违背了这份契约,那就得付出相应的处罚(锅你来背)
在软件开发中,测试被测系统崩溃,产品需求变更,项目进度加快等我们不可抗的因素导致我们测试被拖慢,开发压榨测试时间,测试时间不够(这也是时常发生的问题),后来有一种新的思想--测试替身,不再调用还未开发的api接口,使用测试替身模拟接口的实现(mock就是一种测试替身的手段),但是mock完全不能代替开发写的api,如开发变更了接口,那我们的mock就白写了,而且我们还不知道开发改了,只有调用报错了才知道,那么这样想只要真实api和mock返回数据是一样的就可以解决这个问题吗?这其实是无法保证的
由此诞生了消费者驱动的契约测试(Consumer-Driven Contracts,简称CDC)
一种是根据消费者驱动契约,还有一种是生产者驱动契约,我们主要使用的方式是消费者驱动契约的方式,原因是能够验证服务是否满足我们消费方的期待,本质是从利益者和使用者的角度,体验,目标作为出发点,满足我们一些业务的价值实现.当然生产者驱动契约模式是有的,但我更喜欢两者来维护这份契约,而不是一方提出
为什么要做契约测试?
可能说不上为什么要去做,可能是如今微服务架构复杂,我们平时看百度一下的页面,不单单是测前端哪个按钮能点不能点,背后大量的api调用,大量的微服务嵌套关系,这是如今各种高内聚低耦合的服务架构发展,而且所有开发人员来自不同城市,不同地区,不得不指定契约来做这些事情,如不同区域调用同一份契约,一个接口从1.0变更为2.0,我们需要约定俗成,2.0必须要覆盖1.0的测试需求,而且保证所有的返回数据都能测试到,那样我们说契约测试才会有效果
所以说白了来说就是我们消费者提供一个json/yml格式的文件,再由生产者来实现契约,这样之后就能够实现我们之间说的两者之间的同步问题,当版本迭代或者是接口更新时,新版本中的契约同步是通过旧版接口测试新版的接口,当测试通过后再更新新版本的契约,上线时切换到新接口中
契约测试的不适合场景
公共api服务
性能测试
消费者和生产者没有沟通渠道
契约测试工具
Pact
基于json文件格式
Swagger
基于yml文件格式
这里我来着重学swagger,首先使用方便,对python-django,flask都有扩展,其次它可以根据注释来生成文档,比如github上一些api接口文档开发人员不会开发完再回头重新写一遍文档,这种都是用工具自动就可以生成的,当然也可以用yml文档生成,安装生成也很简单,只要node.js和相关扩展就行了,当然如果不急着做契约测试,做出一份漂亮的接口文档给开发看,心里也是乐滋滋的
# pip install flasgger # 简易示例 import random from flask import Flask, jsonify, request from flasgger import Swagger, swag_from import json app = Flask(__name__) Swagger(app) #swagger为本地文档yml文件的情况 @app.route('/aaa/<string:language>/', methods=['GET']) @swag_from("api_get.yml") def aaa(language): language = language.lower().strip() features = [ "awesome", "great", "dynamic", "simple", "powerful", "amazing", "perfect", "beauty", "lovely" ] size = int(request.args.get('size', 1)) if language in ['php', 'vb', 'visualbasic', 'actionscript']: return "An error occurred, invalid language for awesomeness", 500 return jsonify( language=language, features=random.sample(features, size) ) #多methods的情况 @app.route('/test/', methods=['POST', 'GET', ]) def test(): ''' 这里写接口简单描述信息 这里写详细的接口信息xxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx --- tags: - Awesomeness Language API parameters: - name: username in: path type: string required: true description: 这里填入参描述 姓名 - name: password in: query type: int description: 这里填入参描述 密码 responses: 500: description: 这里填描述 500了 200: description: A ok了 ''' if request.method.lower() == 'get': res = {'status': '这是get请求'} elif request.method.lower() == 'post': res = {'status': '这是post请求'} else: res = {'status': '不支持该方法'} return json.dumps(res,ensure_ascii=False,indent=4) #带参数的get请求 @app.route('/api/<string:language>/', methods=['GET']) def index(language): """ This is the language awesomeness API Call this api passing a language name and get back its features --- tags: - Awesomeness Language API parameters: - name: language in: path type: string required: true description: The language name - name: size in: query type: integer description: size of awesomeness responses: 500: description: Error The language is not awesome! 200: description: A language with its awesomeness schema: id: awesome properties: language: type: string description: The language name default: Lua features: type: array description: The awesomeness list items: type: string default: ["perfect", "simple", "lovely"] """ language = language.lower().strip() features = [ "awesome", "great", "dynamic", "simple", "powerful", "amazing", "perfect", "beauty", "lovely" ] size = int(request.args.get('size', 1)) if language in ['php', 'vb', 'visualbasic', 'actionscript']: return "An error occurred, invalid language for awesomeness", 500 return jsonify( language=language, features=random.sample(features, size) ) app.run(debug=True)
启动flask后,访问http://127.0.0.1:5000/apidocs
我们可以看到这就是我们在测试服务器的用文档制定的契约,我们定义了3个接口,其中多出来的一个是post请求,有点restful风格,这里的接口都是可以点击展开的,点开后如下图
版权声明:本文原创发表于 博客园,作者为 RainBol 本文欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则视为侵权。