微服务契约测试框架-Pact
契约测试
契约测试的思想就是将原本的 Consumer 与 Provider 间同步的集成测试,通过契约进行解耦,变成 Consumer 与 Provider 端两个各自独立的、异步的单元测试。
契约测试的优点:
契约测试与单元测试以及其它测试之间没有重复,它是单纯验证Provider与Consumer之间按预期的方式交互,定位准确;不需要部署真实的系统环境、Mock机制、没有真实API调用,运行非常快、反馈及时、修复周期短、成本低,在这种情况下,自动化测试流水线运行更快了,产品流水线出产品安装包也更快。因此,显然契约测试才是真正对的选择。
契约测试的缺点:
- 契约测试无法做安全或性能测试等。
- 契约测试采用Mock机制,所以没有集成测试更接近真实环境,也不能给业务人员做验收,可视性差。
- 契约测试基于不同的服务使用的协议不同,验证契约的复杂度会不同,复杂度过高时,需要权衡是否有必要加契约测试。
别再加端到端集成测试了,快换契约测试吧 - 简书 (jianshu.com)
基于Consumer驱动的契约测试分两个阶段:
- Consumer生成契约,开发者在Consumer端写测试时Mock掉Provider,运行测试生成契约文件;
- Provider验证契约,开发者拿契约文件直接在Provider端运行测试进行验证。
- 契约测试实践篇
Pact和Spring Cloud Contracts是目前最常用的契约测试框架, Pact 实现就采用 Consumer-driven Contract Testing
Pact
Overview | Pact Docs
Pact 是事实上的 API 合约测试工具。用快速、可靠且易于调试的单元测试取代昂贵且脆弱的端到端集成测试。
- ⚡ 闪电般的速度
- 🎈 轻松的全栈集成测试 - 从前端到后端
- 🔌 支持 HTTP/REST 和事件驱动系统
- 🛠️ 可配置的模拟服务器
- 😌 强大的匹配规则可防止测试变脆
- 🤝 与 Pact Broker / PactFlow 集成,实现强大的 CI/CD 工作流程
- 🔡 支持12+种语言
为什么使用契约?
使用 Pact 进行合同测试可让您:
- ⚡ 本地测试
- 🚀 部署速度更快
- ⬇️ 缩短变更提前期
- 💰 降低 API 集成测试的成本
- 💥 防止中断性更改
- 🔎 了解您的系统使用情况
- 📃 免费记录您的 API
- 🗄 无需复杂的数据夹具
- 🤷 ♂️ 减少对复杂测试环境的依赖
克隆项目
pact有不同语言的版本,这里用的js语言
git clone https://github.com/pact-foundation/pact-js.git
消费者测试
Consumer Tests | Pact Docs
针对消费者完成单元测试,使用测试替身mock server,使单元测试可以通过,并生成契约文件。
(主要是定义契约文件)
运行单个示例
- 切换到所需的示例文件夹
cd examples/v3/typescript - 安装所有示例依赖项
npm install - 运行所有示例 -
npm run test
运行后成功显示通过一条用例
问题:运行npm run test提示Cannot find module '@pact-foundation/pact' or its corresponding type declarations.
解决办法:在pact-js目录下执行npm install @pact-foundation/pact,然后再运行npm run test

运行后会生成pacts目录
该目录下生成的是契约文件
消费者是User Web,生产者是User API
{"consumer": {"name": "User Web"},"interactions": [{"description": "a request to get a user","providerStates": [{"name": "a user with ID 1 exists"}],"request": {"method": "GET","path": "/users/1"},"response": {"body": {"age": 25,"id": 1,"name": "东方不败","province": "河南"},"headers": {"content-type": "application/json"},"matchingRules": {"body": {"$": {"combine": "AND","matchers": [{"match": "type"}]}},"header": {}},"status": 200}}],"metadata": {"pact-js": {"version": "12.1.0"},"pactRust": {"ffi": "0.4.0","models": "1.0.4"},"pactSpecification": {"version": "3.0.0"}},"provider": {"name": "User API"}
}
源码
index.ts文件
import axios, { AxiosPromise } from 'axios';export class UserService { //export关键字表示将该类导出为一个模块的公共接口,使其能够在其他模块中被引用和使用。constructor(private url: string) {}public getUser = (id: number): AxiosPromise => {return axios.request({baseURL: this.url,headers: { Accept: 'application/json' },method: 'GET',url: `/users/${id}`,});};
}
user.spec.ts
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as path from 'path';
import * as sinonChai from 'sinon-chai';
import { PactV3, MatchersV3, LogLevel } from '@pact-foundation/pact';
import { UserService } from '../index';
const { like } = MatchersV3;
const LOG_LEVEL = process.env.LOG_LEVEL || 'TRACE';const expect = chai.expect;chai.use(sinonChai);
chai.use(chaiAsPromised);describe('The Users API', () => {let userService: UserService; //声明了一个变量userService并指定了它的类型为UserService。// 创建两个应用之间的契约const provider = new PactV3({ //pact提供的类consumer: 'User Web',provider: 'User API',logLevel: LOG_LEVEL as LogLevel,});const userExample = { id: 1, name: '东方不败',age:25,province:"河南" }; //契约const EXPECTED_BODY = like(userExample);// 定义测试套件describe('get /users/:id', () => {it('returns the requested user', () => {// 定义测试用例1 一个it是一个测试用例// provider.given('a user with ID 1 exists').uponReceiving('a request to get a user').withRequest({ //请求信息method: 'GET',path: '/users/1',}).willRespondWith({ //响应信息status: 200,headers: { 'content-type': 'application/json' },body: EXPECTED_BODY,});return provider.executeTest(async (mockserver) => { //执行测试// ActuserService = new UserService(mockserver.url); //模拟了一个urlconst response = await userService.getUser(1); //获取到response// 校验response的data与契约相同expect(response.data).to.deep.eq(userExample);});});});
});
原理
1、消费者使用pact提供的mock完成单元测试
2、pact把交互写入契约文件(通常是一个json文档)
3、消费者将契约分布给中间人(或者分享出去)
4、pact收到契约,并使用本地运行的provider重放请求
5、提供者在契约测试中需要去除依赖,来确保测试更快速和确定。
在pact-js的doc目录可以看到用户手册。
Consumer API有很多属性:
| `new PactV3(options)` | 为proverder API创建mock server test替身
| `addInteraction(...)` | `V3Interaction` | 注册交互
| `given(...)` | `ProviderStateV3` | 交互中提供者的状态 |
| `uponReceiving(...)` | string | 场景名称,在契约文件中given和uponReceiving必须唯一。
| `withRequest(...)` | `V3Request` | The HTTP 请求信息
| `willRespondWith(...)` | `V3Response` | The HTTP响应信息 |
| `executeTest(...)` | - |执行用户定义函数,如果执行成功,会更新契约文件。
new PactV3的构造参数:
| Parameter | Required? | Type | Description
| ------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------
| `consumer` | yes | string | 消费者名称
| `provider` | yes | string | 生产者名称
| `port` | no | number | 运行mock服务的端口,默认是随机
| `host` | no | string | 运行mock服务的地址, defaults to 127.0.0.1
| `tls` | no | boolean | 系诶一 (default false, HTTP)
| `dir` | no | string | 契约文件输出目录
| `log` | no | string | 日志文件
| `logLevel` | no | string | 日志级别Log level: one of 'trace', 'debug', 'info', 'error', 'fatal' or 'warn'
| `spec` | no | number | Pact的版本 (defaults to 2)
| `cors` | no | boolean |允许跨域,默认是false
| `timeout` | no | number | The time to wait for the mock server tq5o start up in milliseconds. Defaults to 30 seconds (30000)
第一步是为Consumer API创建一个test
例子采用的是Mocha框架
1)创建契约项目
2)启动Mock provider来替代真正的Provider
3 ) 添加消费者期望结果
4)完成test
5) 验证Consumer和Mock service之间产生的交互(即运行代码,看是否pass)
6)产生契约文件 (代码运行完就会产生契约文件)
生产者测试
Matching | Pact Docs
一个provider测试的输入是一个或者多个契约文件,Pact验证provider符合这些契约文件。
在简单的示例下,可以使用本地契约文件验证provider,但在实际使用时,应该使用Pact Broker来管理契约或者CI/CD工作流
1、启动本地的Provider服务
2、可选的,检测 API 以配置提供程序状态
3、运行provider验证步骤
一旦为消费者创建了契约,就应该用Provider来验证这些契约。Pact提供了如下API。

Verification Options:
| 参数 | 是否必须 | 类型 | 描述 |
| providerBaseUrl | TRUE | string | provider的基础url |
| pactBrokerUrl | false | string | pact broker的base url |
| provider | false | string | provider的name |
| consumerVersionSelectors | false | ConsumerVersionSelector|array | pe配置验证的版本 |
| consumerVersionTags | false | string|array | 使用标签取出最新的契约 |
| providerVersionTags | FALSE | string|array | 应用到provider的标签 |
| providerVersionBranch | FALSE | string | 分支 |
| includeWipPactsSince | FALSE | string | |
| pactUrls | FALSE | array | 本地契约文件路径数组或者基于HTTP的url,如果不用Pact Broker则该项必须 |
| providerStatesSetupUrl | FALSE | string | 该参数已废弃 |
| stateHandlers | FALSE | object | |
| requestFilter | FALSE | function (Express middleware) | 改变请求或者输出 |
| beforeEach | FALSE | function | 在每一个交互验证前执行的函数 |
| afterEach | FALSE | function | 在每一个交互验证后执行的函数 |
| pactBrokerUsername | FALSE | string | Pact Broker的验证username |
| pactBrokerPassword | FALSE | string | Pact Broker的验证password |
| pactBrokerToken | FALSE | string | Pact Broker的验证token |
| publishVerificationResult | FALSE | boolean | 发布验证结果至Broker,只有在持续集成时才设置这个参数 |
| providerVersion | FALSE | string | provider的版本 |
| enablePending | FALSE | boolean | 挂起契约 |
| timeout | FALSE | number | 超时时间,默认是30秒 |
| logLevel | FALSE | string | 不需要,log级别在环境变量中设置 |
最好将契约验证测试作为单元测试套件的一部分,因为可以很方便的使用stubbing,lac或者其他工作。
const { Verifier } = require('@pact-foundation/pact');// (1) Start provider locally. Be sure to stub out any external dependencies
server.listen(8081, () => {importData();console.log('Animal Profile Service listening on http://localhost:8081');
});// (2) Verify that the provider meets all consumer expectations
describe('Pact Verification', () => {it('validates the expectations of Matching Service', () => {let token = 'INVALID TOKEN';return new Verifier({providerBaseUrl: 'http://localhost:8081', // <- location of your running providerpactUrls: [ path.resolve(process.cwd(), "./pacts/SomeConsumer-SomeProvider.json") ],}).verifyProvider().then(() => {console.log('Pact Verification Complete!');});});
});
匹配规则
可以使用正则表达式或者基于对象类型匹配或者数组来验证相应的结构
匹配规则取决于契约文件
相关文章:
微服务契约测试框架-Pact
契约测试 契约测试的思想就是将原本的 Consumer 与 Provider 间同步的集成测试,通过契约进行解耦,变成 Consumer 与 Provider 端两个各自独立的、异步的单元测试。 契约测试的优点: 契约测试与单元测试以及其它测试之间没有重复,…...
LightGlue论文翻译
LightGlue:光速下的局部特征匹配 摘要 - 我们介绍 LightGlue,一个深度神经网络,学习匹配图像中的局部特征。我们重新审视 SuperGlue 的多重设计决策,稀疏匹配的最新技术,并得出简单而有效的改进。累积起来,它们使 Lig…...
iOS开发-CAShapeLayer与UIBezierPath实现微信首页的下拉菜单效果
iOS开发-CAShapeLayer与UIBezierPath实现微信首页的下拉菜单效果 之前开发中遇到需要使用实现微信首页的下拉菜单效果。用到了CAShapeLayer与UIBezierPath绘制菜单外框。 一、效果图 二、CAShapeLayer与UIBezierPath 2.1、CAShapeLayer是什么? CAShapeLayer继承自…...
《Elasticsearch 源码解析与优化实战》第5章:选主流程
《Elasticsearch 源码解析与优化实战》第5章:选主流程 - 墨天轮 一、简介 Discovery 模块负责发现集群中的节点,以及选择主节点。ES 支持多种不同 Discovery 类型选择,内置的实现称为Zen Discovery ,其他的包括公有云平台亚马逊的EC2、谷歌…...
Spring Cloud Alibaba - Nacos源码分析(三)
目录 一、Nacos客户端服务订阅的事件机制 1、监听事件的注册 2、ServiceInfo处理 serviceInfoHolder.processServiceInfo 一、Nacos客户端服务订阅的事件机制 Nacos客户端订阅的核心流程:Nacos客户端通过一个定时任务,每6秒从注册中心获取实例列表&…...
DOCKER镜像和容器
1.前言 初见DOCKER,感觉和我们常用的虚拟机(VMware,viurebox)类似,是一个独立于宿主机的模块,可以解决程序在各个系统间的移植,但它真的仅仅是这样嘛? 2.容器的优缺点 1.1.容器…...
探索网页原型设计:构建出色的用户体验
在当今数字化时代,用户对网页体验的要求日益提高。在网页设计过程中,扮演着至关重要的角色。通过网页原型设计,产品经理能够更好地展示和传达网页的整体布局、导航结构、元素位置和交互效果,从而使团队成员更清晰地了解设计意图&a…...
48,排序算法merge
功能描述: 两个容器元素合并,并储存到另一容器中 函数原型: merge(iterator beg1,iterator end1,iterator beg2,iterator end2,iterator dest); //容器元素合并,并存储到另一个容器中 //注意:两个容器必须是有序的…...
【MySQL】复合查询
复合查询目录 一、基本查询二、多表查询三、自连接四、子查询4.1 单行子查询4.2 多行子查询4.3 多列子查询4.4 在from子句中使用子查询4.5 合并查询4.5.1 union4.5.2 union all 五、实战OJ 一、基本查询 --查询工资高于500或岗位为MANAGER的雇员,同时还要满足他们的…...
JavaScript中的this指向及绑定规则
在JavaScript中,this是一个特殊的关键字,用于表示函数执行的上下文对象,也就是当前函数被调用时所在的对象。由于JavaScript的函数调用方式多种多样,this的指向也因此而变化。本文将介绍JavaScript中this的指向及绑定规则…...
css中预编译理解,它们之间区别
css预编译? css预编译器用一种专门的编程语言,它可以对web页面样式然后再编译成正常css文件,可以更加方便和高效的编写css代表。主要作用就是为css提供了变量,函数,嵌套,继承,混合等功能&#…...
如何使用Java处理JSON数据?
在Java中,您可以使用许多库来处理JSON数据。以下是使用一种常见的库 Gson 的示例: 首先,确保您已经将 Gson 库添加到您的项目中。您可以在 Maven 中添加以下依赖项: <dependency><groupId>com.google.code.gson<…...
java设计模式-观察者模式
什么是观察者模式 观察者模式(Observer)是软件设计中的一种行为模式。 它定义了对象之间的一对多关系,其中如果一个对象改变了状态,所有依赖它的对象都会自动被通知并更新。 这种模式包含了两种主要的角色,即被观察…...
HiveSQL SparkSQL中常用知识点记录
目录 0. 相关文章链接 1. hive中多表full join主键重复问题 2. Hive中选出最新一个分区中新增和变化的数据 3. Hive中使用sort_array函数解决collet_list列表排序混乱问题 4. SQL中对小数位数很多的数值转换成文本的时候不使用科学计数法 5. HiveSQL & SparkSQL中炸裂…...
mac不识别移动硬盘导致无法拷贝资源
背景 硬盘插入到Mac电脑上之后,mac不识别移动硬盘导致无法拷贝资源。 移动硬盘在Mac上无法被识别的原因可能有很多,多数情况下,是硬盘的格式与Mac电脑不兼容。 文件系统格式不兼容 macOS使用的文件系统是HFS或APFS,如果移动硬盘是…...
Opencv的Mat内容学习
来源:Opencv的Mat内容小记 - 知乎 (zhihu.com) 1.Mat是一种图像容器,是二维向量。 灰度图的Mat一般存放<uchar>类型 RGB彩色图像一般存放<Vec3b>类型。 (1)单通道灰度图数据存放样式: (2)RGB三通道彩色图存放形式不同&#x…...
MySQL~数据库的设计
二、数据库的设计 1、多表之间的关系 1.1 三种分类 一对一: 分析:一个人只有一个身份证,一个身份证只能对应一个人 如:人和身份证 一对多: 如:部门和员工 分析:一个部门有多个员工ÿ…...
开源了!最强原创图解八股文面试网来袭
强烈推荐 Github上业内新晋的一匹黑马—Java图解八股文面试网—Java2Top.cn,图解 Java 大厂面试题,深入全面,真的强烈推荐~ 这是一个二本逆袭阿里的大佬根据自己秋招上岸所看过的相关专栏,面经,课程,结合自…...
微信小程序开发6
一、分包-基础概念 1.1、什么是分包 分包指的是把一个完整的小程序项目,按照需求划分为不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。 1.2、分包的好处 对小程序进行分包的好处主要有以下两点: 可以优化小程序…...
JS 根据身份证号获取年龄、性别、出生日期
先说一代身份证和二代身份证的区别: 1.编号位数不同,第一代身份证为15位号码,第二代证是18位号码 2.编码规则不同,第一代身份证在前6位号码后没有完整出生年份,而二代的有完整的出生年份,一代身份证将年份前二位省略…...
7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...
基于当前项目通过npm包形式暴露公共组件
1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹,并新增内容 3.创建package文件夹...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
ffmpeg(四):滤镜命令
FFmpeg 的滤镜命令是用于音视频处理中的强大工具,可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下: ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜: ffmpeg…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
MySQL用户和授权
开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务: test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...
