微服务契约测试框架-Pact

news2024/9/25 21:29:58

契约测试

契约测试的思想就是将原本的 Consumer 与 Provider 间同步集成测试,通过契约进行解耦,变成 Consumer 与 Provider 端两个各自独立的、异步单元测试

契约测试的优点:

契约测试与单元测试以及其它测试之间没有重复,它是单纯验证Provider与Consumer之间按预期的方式交互,定位准确;不需要部署真实的系统环境、Mock机制、没有真实API调用,运行非常快、反馈及时、修复周期短、成本低,在这种情况下,自动化测试流水线运行更快了,产品流水线出产品安装包也更快。因此,显然契约测试才是真正对的选择。

契约测试的缺点:

  • 契约测试无法做安全或性能测试等。
  • 契约测试采用Mock机制,所以没有集成测试更接近真实环境,也不能给业务人员做验收,可视性差。
  • 契约测试基于不同的服务使用的协议不同,验证契约的复杂度会不同,复杂度过高时,需要权衡是否有必要加契约测试。

别再加端到端集成测试了,快换契约测试吧 - 简书 (jianshu.com)

基于Consumer驱动的契约测试分两个阶段:

  1. Consumer生成契约,开发者在Consumer端写测试时Mock掉Provider,运行测试生成契约文件;
  2. Provider验证契约,开发者拿契约文件直接在Provider端运行测试进行验证。
  • 契约测试实践篇

PactSpring 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,使单元测试可以通过,并生成契约文件。

(主要是定义契约文件) 

运行单个示例

  1. 切换到所需的示例文件夹cd examples/v3/typescript
  2. 安装所有示例依赖项npm install 
  3. 运行所有示例 -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) => { //执行测试
        // Act
        userService = new UserService(mockserver.url); //模拟了一个url
        const 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  TRUEstringprovider的基础url
pactBrokerUrlfalsestringpact broker的base url
providerfalsestringprovider的name
consumerVersionSelectorsfalseConsumerVersionSelector|arraype配置验证的版本
consumerVersionTags falsestring|array使用标签取出最新的契约
providerVersionTagsFALSEstring|array应用到provider的标签
providerVersionBranchFALSEstring分支
includeWipPactsSinceFALSEstring
pactUrlsFALSEarray本地契约文件路径数组或者基于HTTP的url,如果不用Pact Broker则该项必须
providerStatesSetupUrlFALSEstring该参数已废弃
stateHandlersFALSEobject
requestFilterFALSEfunction (Express middleware)改变请求或者输出
beforeEachFALSEfunction在每一个交互验证前执行的函数
afterEachFALSEfunction在每一个交互验证后执行的函数
pactBrokerUsernameFALSEstringPact Broker的验证username
pactBrokerPasswordFALSEstringPact Broker的验证password
pactBrokerTokenFALSEstringPact Broker的验证token
publishVerificationResultFALSEboolean发布验证结果至Broker,只有在持续集成时才设置这个参数
providerVersionFALSEstringprovider的版本
enablePendingFALSEboolean挂起契约
timeoutFALSEnumber超时时间,默认是30秒
logLevelFALSEstring不需要,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 provider
      pactUrls: [ path.resolve(process.cwd(), "./pacts/SomeConsumer-SomeProvider.json") ],
    })
      .verifyProvider()
      .then(() => {
        console.log('Pact Verification Complete!');
      });
  });
});

匹配规则

可以使用正则表达式或者基于对象类型匹配或者数组来验证相应的结构
匹配规则取决于契约文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/797875.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java商城系统和php商城系统有什么差异?如何选择?

java商城系统和php商城系统是两种常见的电子商务平台&#xff0c;它们都具有一定的优势和劣势。那么&#xff0c;java商城系统和php商城系统又有哪些差异呢&#xff1f; 一、开发难度 Java商城系统和PHP商城系统在开发难度方面存在一定的差异。Java商城系统需要使用Java语言进…

小红书课程发光社群知识库,点亮哥专为超级个体设计解决方案

小红书课程点亮哥知识库 开创了学习小红书教育培训先河 针对超级个体轻创业的学习需求场景 创新推出了“知识库全新学习方式”。 一个人如何做好小红书? 超级个体轻创业,如何做好小红书? 通过打造个人IP、或者塑造老板个人品牌,来实现互联网变现,如何做好小红书? 就像挑…

系统架构设计师-软件架构设计(5)

目录 一、构件与中间件技术 1、软件复用 2、构件与中间件技术的概念 3、构件的复用 3.1 检索与提取构件 3.2 理解与评价构件 3.3 修改构件 3.4 组装构件 4、中间件 4.1 采用中间件技术的优点&#xff1a; 4.2 中间件的分类&#xff1a; 5、构件标准 5.1 CORBA&#xff08;公共…

Android 电子称定标流程

1、首先确保电子称正确安装&#xff0c;底部悬空&#xff0c;托盘悬空。 2、去皮&#xff0c;把去皮数据保存到本地 3、定标、例如拿100克的砝码放入托盘&#xff0c; 获取值-去皮值及得到定标值 4、通过定标值计算出需要设置的满量程&#xff0c;或者计算对应的重量&#x…

vue-element-admin中实现自适应功能

npm install postcss-px-to-viewport --save-dev 项目根目录下建一个名字为 .postcssrc.js 的js文件(前边的.别忘了)&#xff0c;在该文件里写以下代码 //添加如下配置&#xff1a; module.exports {plugins: {autoprefixer: {}, // 用来给不同的浏览器自动添加相应前缀&a…

小学期笔记——天天酷跑4

效果&#xff1a; 点击登录&#xff1a; ------------------------ 效果&#xff1a; 静态的一张图 ------------------------ 完善一下会变成那张静态的图从左往右移动&#xff0c;但是这一张图到后面会拉丝 -------------------- 再完善一下&#xff1a; &#xff08;再…

洗地机有没有必要买?好用的洗地机推荐

随着科技的发展&#xff0c;越来越多的家用电器出现。就比如在清洁家电方面&#xff0c;相继出现了吸尘器、扫地机、洗地机&#xff01;其中洗地机更是近年来爆火的一个智能清洁家电&#xff01;而如果你们和小编一样是个上班族&#xff0c;然后每天下班回家面对脏乱的地板&…

汽车UDS诊断深度学习专栏

1.英文术语 英文术语翻译Diagnostic诊断Onboard Diagnostic 在线诊断 Offboard Diagnostic离线诊断Unified diagnostic service简称 UDS 2.缩写表 缩写解释ISO国际标准化组织UDSUnified diagnostic service&#xff0c;统一的诊断服务ECU电控单元DTC 诊断故障码 ISO14229UD…

Modbus TCP/IP之异常响应

文章目录 一、异常响应二、异常码分析2.1 异常码0x012.2 异常码0x022.3 异常码0x032.4 异常码0x04、0x05等 一、异常响应 对于查询报文&#xff0c;存在以下四种处理反馈&#xff1a; 正常接收&#xff0c;正常处理&#xff0c;返回正常响应报文&#xff1b;因为通信错误等原因…

我对牟长青分享的各个私董会数据分析

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 其实之前&#xff0c;我也想写一个关于各个草根社群的数据分析&#xff0c;但这样的文章容易得罪人&#xff0c;因为我一直喜欢直言不讳&#xff0c;所以一直没有动笔。例如&#xff0c;我在6月份写…

OpenGl中的VAO、VBO与EBO

文章目录 VBO(顶点缓冲区对象)VBO的使用 EBO(索引缓冲对象)EBO的使用 VAO(顶点数组对象)VAO的使用 三者的区别someting。。。 哎&#xff0c;很离谱&#xff0c;上个月学learnopengl学到一半跑去看庄懂老师的视频&#xff0c;结果该还的东西迟早得还&#xff0c;再打开之前的工…

NineData支持最受欢迎数据库PostgreSQL

根据在 Stack Overflow 发布的 2023 开发者调研报告中显示&#xff0c;PostgreSQL 以 45% vs 41% 的受欢迎比率战胜 MySQL&#xff0c;成为新的最受欢迎的数据库。NineData 也在近期支持了 PostgreSQL&#xff0c;用户可以在 NineData 平台上进行创建数据库/Schema、管理用户与…

BTTES,2101505-88-6,是各种化学生物实验中生物偶联的理想选择

资料编辑|陕西新研博美生物科技有限公司小编MISSwu​ 规格单位&#xff1a;g |货期&#xff1a;按照具体的库存进行提供 | 纯度&#xff1a;95% PART1----​试剂描述&#xff1a; BTTES是铜&#xff08;I&#xff09;催化的叠氮化物-炔烃环加成&#xff08;CuAAC&#x…

整数替换(力扣)HashMap + 递归 JAVA

给定一个正整数 n &#xff0c;你可以做如下操作&#xff1a; 如果 n 是偶数&#xff0c;则用 n / 2替换 n 。 如果 n 是奇数&#xff0c;则可以用 n 1或n - 1替换 n 。 返回 n 变为 1 所需的 最小替换次数 。 示例 1&#xff1a; 输入&#xff1a;n 8 输出&#xff1a;3 解释…

系统架构设计师-软件架构设计(4)

目录 一、软件架构评估 1、敏感点 2、权衡点 3、风险点 4、非风险点 5、架构评估方法 5.1 基于调查问卷或检查表的方式 5.2 基于度量的方式 5.3 基于场景的方式 6、基于场景的评估方法 6.1 软件架构分析法&#xff08;SAAM&#xff09; 6.2 架构权衡分析法&#xff08;ATAM&am…

vue 封装一个鼠标拖动选择时间段功能

<template><div class"timeRange"><div class"calendar"><table><thead><tr><th rowspan"6" class"weekRow"><b>周/时间</b></th><th colspan"24"><…

docker容器引擎(三)

docker 一、Docker 的数据管理1&#xff0e;数据卷2&#xff0e;数据卷容器 二、容器互联&#xff08;使用centos镜像&#xff09;三、docker镜像的创建创建镜像的方法DockerfileDocker 镜像结构的分层Dockerfile 操作常用的指令&#xff1a; 四、Dockerfile 案例 一、Docker 的…

Java从入门到精通(一)

Java从入门到精通&#xff08;一&#xff09; 前言 温故而知新&#xff0c;闲着没事干&#xff0c;准备将Java编程语言的知识点从头梳理一遍&#xff0c;整理成笔记&#xff0c;逐篇发布。 部分图片素材来源与B站“黑马程序员”的课程。 一 Java背景 Java是1995年 由Sun公司…

error CS0246: 未能找到类型或命名空间名“Newtonsoft”(是否缺少 using 指令或程序集引用?)

error CS0246: 未能找到类型或命名空间名“Newtonsoft”(是否缺少 using 指令或程序集引用?) 如图&#xff0c;明明已经引用了命名空间&#xff0c;可以点击生成报错。找了很长时间的原因&#xff0c;最后终于解决了问题。 原因是Newtonsoft这个dll文件只支持.net 4.5框架&…

互联网搜索的学习笔记

1. 参考资料 《Internet Search Tips》《Google Search Operators: The Complete List (42 Advanced Operators)》 2. 预备知识 2.1 查询语法 2.1.1 -&#xff1a;排除符 用于排除指定关键字。例如&#xff0c;如果想搜索“苹果”但不想看到“苹果手机”的结果&#xff0c;…