在前一篇博客中我们讲到契约测试是什么,以及它能给我们软件交付带来什么价值,本次将介绍一个开源的契约测试框架Pact,它最初是用ruby语言实现的,后来被js,C#,java,go,python 等语言重写,此文将介绍Pact框架的相关知识并结合示例代码讲解在实际项目中应该怎么使用。

Pact是什么?

Pact是一个开源框架,最早是由澳洲最大的房地产信息提供商REA Group的开发者及咨询师们共同创造。REA Group的开发团队很早便在项目中使用了微服务架构,并在团队中对于敏捷和测试的重要性早已形成共识,因此设计出这样的优秀框架并应用于日常工作中也是十分自然。

Pact工具于2013年开始开源,发展到今天已然形成了一个小的生态圈,包括各种语言(Ruby/Java/.NET/JavaScript/Go/Scala/Groovy...)下的Pact实现,契约文件共享工具Pact Broker等。Pact的用户已经遍及包括RedHat、IBM、Accenture等在内的若干知名公司,Pact已经是事实上的契约测试方面的业界标准。

Pact可以用来做什么?

Pact是支持消费者驱动的契约测试框架,针对微服务的模式下多个单独服务的接口契约测试以及前后端分离的模式提供了很好的支持。

Pact的工作原理

消费者端作为数据的最终使用者非常清楚,明确的知道需要的什么样格式,类型的数据,它将负责创建契约文档(包含结构和格式的json文件),服务提供端将根据消费者端创建的契约文档提供对应格式的数据并返回给消费者,通过契约检查判断如果服务端提供的数据和消费者生成的契约不匹配,将抛出异常并提示给服务端。总结如下:

  1. 在消费者项目代码中编写单元测试,期望响应设置于模拟的服务提供者上。
  2. 在测试运行时,模拟的服务将返回所期望的响应。请求和所期望的响应将会被写入到一个“pact”文件中。
  3. pact文件中的请求随后在提供者上进行重放,并检查实际响应以确保其与所期望响应相匹配。

Pact相关的术语

服务消费者

  服务消费者是指向另一组件(服务提供者)发起HTTP请求的组件。注意这并不依赖于数据的发送方式——无论是GET还是PUT / POST / PATCH消费者都是HTTP请求的发起者。

 服务提供者

  服务提供者是指向另一组件(服务消费者)的HTTP请求提供响应的服务器。

模拟服务提供者

  模拟服务提供者用于在消费者项目中的单元测试里模拟真实的服务提供者,意味着不必需要真实的服务提供者就绪,就可以将类集成测试运行起来。

    Pact文件

  Pact文件是指一个含有消费者测试中所定义的请求和响应被序列化后的JSON的文件,即契约。

Pact验证(契约验证)

  要对一个Pact进行验证,就要对Pact文件中所包含的请求基于提供者代码进行重放,然后检查返回的响应,确保其与Pact文件中所期望响应相匹配。

    提供者状态

  在对提供者重放某个给定的请求时,一个用于描述此时提供者应具有的“状态”(类似于夹具)的名字——比如“when user ken does not exists”或“when user ken has a bank account”。

  提供者状态的名字是在写消费者测试时被指定的,之后当运行提供者的pact验证时,这个名字将被用于唯一标识在请求执行前应运行的代码块。

Pact适用的场景

当你的团队同时负责开发服务消费者与服务提供者,并且服务消费者的需求被用来驱动服务提供者的功能时,Pact对于在服务集成方面进行设计和测试是最具价值 的。它是组织内部                 开发和测试微服务,前后端分离项目的绝佳工具。

Pact不适用的场景

  • 性能和压力测试。
  • 服务提供者的功能测试——这是服务提供者自己的测试应该做的。Pact是用来检查请求和响应的内容及格式。
  • 当你在必须使用实际测试的API才能将数据载入服务提供者的情况下,因为你的服务提供者中存在了无法mock的第三方的依赖
  • “透传”API的测试,是指服务提供者仅将请求内容传递到下游服务而不做任何验证。

Pact使用实例

  下面将展示代码示例,这是一个前后端分离的项目,前端使用javascript访问后端api获取数据,后端使用.net WebApi 提供数据的返回

 后端代码:

  新建BookingController,返回一个预定对象的信息,访问地址: http://localhost:51502/api/booking

public class BookingController : ApiController
{
// GET: Booking
[HttpGet]
public BookingModel Get()
{
return new BookingModel()
{
Id = ,
FirstName = "Ken",
LastName = "Wang",
Users = new List<User>()
{
new User()
{
Name = "asd",
Age = ""
},
new User()
{
Name = "asd",
Age = ""
},
new User()
{
Name = "kenwang",
Age = "",
Address = "shangxi road"
}
}
};
}
}

BookingModel 实体定义如下:

public class BookingModel
{
public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public List<User> Users { get; set; }
} public class User
{
public string Name { get; set; } public string Age { get; set; } public string Address { get; set; }
}

返回对象格式如下:

{
"Id": 12,
"FirstName": "Ken",
"LastName": "Wang",
"Users": [
{
"Name": "asd",
"Age": "1",
"Address": 0
},
{
"Name": "asd",
"Age": "1",
"Address": 0
},
{
"Name": "kenwang",
"Age": "223",
"Address": "shanxi road"
}
]
}

服务端就好了,下面看消费端实现

client.js 负责发起调用请求来获取数据:

const request = require('superagent')
const API_HOST = process.env.API_HOST || 'http://localhost'
const API_PORT = 51502
const moment = require('moment')
const API_ENDPOINT = `${API_HOST}:${API_PORT}` // Fetch provider data
const fetchProviderData = () => {
return request
.get(`${API_ENDPOINT}/api/booking`)
.then((res) => {
var Users = [];
Users.push({
Name: 'user1',
Age : '11'
}); Users.push({
Name: 'asd',
Age : '1'
}); return {
Id: 12,
FirstName: 'ken',
LastName: 'wang',
Users: Users
}
}, (err) => {
throw new Error(`Error from response: ${err.body}`)
})
} module.exports = {
fetchProviderData
}

consumer.js负责调用client.js的方法获取数据,拿到数据之后记录日志

const client = require('./client')

client.fetchProviderData().then(response => {
console.log(response)
}, error => {
console.error(error)
})

添加client.js的测试代码,前面的工作原理部分讲到契约的生成是依赖于消费者端的测试代码而生成,也就是说消费者端通过单元测试既覆盖了代码逻辑,又帮助我们生成了契约文件。

consumerPact.spec.js文件是对client的测试:

const chai = require('chai')
const path = require('path')
const chaiAsPromised = require('chai-as-promised')
const pact = require('pact')
const expect = chai.expect
const API_PORT = process.env.API_PORT || 51502
const {
fetchProviderData
} = require('../client')
chai.use(chaiAsPromised) // Configure and import consumer API
// Note that we update the API endpoint to point at the Mock Service
const LOG_LEVEL = process.env.LOG_LEVEL || 'WARN' const provider = pact({
consumer: 'Consumer Demo',
provider: 'Provider Demo',
port: API_PORT,
log: path.resolve(process.cwd(), 'logs', 'pact.log'),
dir: path.resolve(process.cwd(), 'pacts'),
logLevel: LOG_LEVEL,
spec: 2
})
// Alias flexible matchers for simplicity
const { somethingLike: like,eachLike: eachLike, term } = pact.Matchers describe('Pact with Our Provider', () => {
before(() => {
return provider.setup()
}) describe('given data count > 0', () => {
describe('when a call to the Provider is made', () => {
describe('and a valid date is provided', () => {
before(() => {
return provider.addInteraction({
uponReceiving: 'a request for JSON data',
withRequest: {
method: 'GET',
path: '/api/booking'
},
willRespondWith: {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: {
Id: like(10),
FirstName: like('ken'),
LastName: like('wang'),
Users: eachLike({
"Name": like('test'),
"Age": like('10')
},{min:1})
}
}
})
}) it('can process the JSON payload from the provider', done => {
const response = fetchProviderData() expect(response).to.eventually.have.property('Id', 10)
}) it('should validate the interactions and create a contract', () => {
return provider.verify()
})
}) })
}) // Write pact files to file
after(() => {
return provider.finalize()
})
})

okay,消费者端的代码已经完成,我们来执行一下consumer.js,成功之后便会生成对应的contract文件,如下:

{
"consumer": {
"name": "Consumer Demo"
},
"provider": {
"name": "Provider Demo"
},
"interactions": [
{
"description": "a request for JSON data",
"providerState": "data count > 0",
"request": {
"method": "GET",
"path": "/api/booking"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"body": {
"Id": 10,
"FirstName": "ken",
"LastName": "wang",
"Users": [
{
"Name": "test",
"Age": "10"
}
]
},
"matchingRules": {
"$.body.Id": {
"match": "type"
},
"$.body.FirstName": {
"match": "type"
},
"$.body.LastName": {
"match": "type"
},
"$.body.Users": {
"min": 1
},
"$.body.Users[*].*": {
"match": "type"
},
"$.body.Users[*].Name": {
"match": "type"
},
"$.body.Users[*].Age": {
"match": "type"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}

这就是需要消费端需要的数据格式,而作为服务提供者提供给消费者的数据必须满足这样的约束,否则就是测试失败的,下面我们建立一个C# 的contract test的工程,然后测试消费端和提供端是否匹配统一的契约。测试工程需要引用xUnit 和 PactNet的Nuget包,直接从Nuget server下载安装就可以了,会把所有的依赖都添加进来。

新建BookingContractApiTesting 的class:

private readonly ITestOutputHelper _output;

        public RetriveBookingApiContractTesting(ITestOutputHelper output)
{
_output = output;
} [Fact]
public void EnsureEventApiHonoursPactWithConsumer()
{
const string serviceUri = "http://localhost:51502"; var config = new PactVerifierConfig
{
Outputters = new List<IOutput>
{
new XUnitOutput(_output)
},
Verbose = false
}; IPactVerifier pactVerifier = new PactVerifier(config);
pactVerifier
.ServiceProvider("Event API", serviceUri)
.HonoursPactWith("Event API Consumer")
.PactUri("userclient-userservice.json")
.Verify();
}

写完之后我们来运行一下,结果显示通过:

从上面的Api返回的字段来看我们其实是多给消费端返回了一个Address字段,但是契约检查并没有报错,这说明契约检查时是按照最小原则检查的,即使是api多返回数据依然是可以的,但是如果api返回的字段中少了契约中的字段,那会怎样呢,我们来试着删除掉api返回的Id字段。重启api之后我们再跑一遍测试,结果显示如下:

运行结果会显示实际返回的和期望的差异,这就达到了契约测试的目的。

Pact 匹配规则

我们可以看到生成的contract文件中有matchingRules 的节点,这个节点下面就是为了添加匹配规则的,目前支持四种匹配方式:

正则匹配:

      将执行正则表达式匹配值的字符串表示

 类型匹配:

   将根据值执行一个类型的匹配,也就是说,如果它们是相同的类型,则它们是相等的

 元素最小长度匹配:

  根据值执行一个类型的匹配,也就是说,如果它们是相同的类型,则它们是相等的。此外,如果值表示集合,则实际值的长度与最小值进行比较。

 集合最大长度匹配:

  根据值执行一个类型的匹配,也就是说,如果它们是相同的类型,则它们是相等的。此外,如果值表示集合,则实际值的长度与最大值进行比较。

类型匹配只适用于一些简单类型的匹配,负责类型,如邮箱等需要用正则来匹配。

写在最后

  内容就介绍到这里,如果大家有更好的经验,欢迎分享交流。

  学习参考:

    https://docs.pact.io/

    https://github.com/pact-foundation/pact-net.git

    https://github.com/cwilcox-fl/Pact-Net-Core.git

    https://github.com/pact-foundation/pact-js.git

契约测试框架-Pact实践的更多相关文章

  1. 为什么要抛弃Pact?如何快速实现契约测试(CDC)

    前言 在前几天的博客中,我转载了一篇文章,其中介绍了契约测试和pact是怎么实施的,的确很有帮助.但我经过研究,其实是pact本身也是有缺陷的,结合我近期在使用的服务型工具和我的实际情况,觉得实现契约 ...

  2. 基于Pact的契约测试

    背景 如今,契约测试已经逐渐成为测试圈中一个炙手可热的话题,特别是在微服务大行其道的行业背景下,越来越多的团队开始关注服务之间的契约及其契约测试. 什么是契约测试     关于什么是契约测试这个问题, ...

  3. 微服务下的契约测试(CDC)解读

    1. 前言 有近两周没有在公众号中发表文章了,看过我之前公众号的读者都知道,公众号中近期在连载<RobotFramework接口自动化系列课程>,原本计划每周更新一篇,最近由于博主在带一个 ...

  4. iOS开发中的测试框架

    转载作者:@crespoxiao 我们为什么要用测试框架呢?当然对项目开发有帮助了,但是业内现状是经常赶进度,所以TDD还是算了吧,BDD就测测数据存取和重要环节,这很重要,一次性跑完测试单元检查接口 ...

  5. iOS开发中的测试框架 (转载)

      作者:CrespoXiao授权 地址:http://www.jianshu.com/p/7e3f197504c1 我们为什么要用测试框架呢?当然对项目开发有帮助了,但是业内现状是经常赶进度,所以T ...

  6. 使用 Spring 2.5 TestContext 测试框架

    Spring 2.5 TestContext 测试框架用于测试基于 Spring 的程序,TestContext 测试框架和低版本 Spring 测试框架没有任何关系,是一个全新的基于注解的测试框架, ...

  7. BDD测试框架Spock概要

    前言 为了找到一个适合自己的.更具操作性的.以DDD为核心的开发方法,我最近一直在摸索如何揉合BDD与DDD.围绕这个目标,我找到了Impact Mapping → Cucumber → Spock ...

  8. [转] ScalaTest测试框架

    [From] https://blog.csdn.net/hany3000/article/details/51033610 ScalaTest测试框架 2016年04月01日 02:49:35 阅读 ...

  9. 最受欢迎的5款Node.js端到端测试框架

    测试,尤其是自动化测试在现代 WEB 工程中有着非常重要的角色,与交付过程集成良好的自动化测试流程可以在新版发布时帮你快速回归产品功能,也可以充当产品文档.测试因粒度不同又可以分为单元测试.接口测试. ...

随机推荐

  1. 求原码、补码,反码(C语言源代码)

    #include <stdio.h> #define N 8 //这里你要求是8位 int main(int argc, const char * argv[]) { int binary ...

  2. C#方法中参数ref和out的解析

    一.C#方法中参数类型 有4种参数类型,有时候很难记住它们的不同特征,下图对它们做一个总结,使之更容易比较和对照. 二.C#方法中的参数 1.值参数 使用值参数,通过复制实参的值到形参的方式把数据传递 ...

  3. EF 数据重复和缺失问题(select 错误 )

    字段有 id,name,password,sex 1.错误举例: var data = db.User.Select(d => d):   2修正 var data = db.User.Sele ...

  4. 我们的代码为什么要压缩成7z?

    代码为什么要压缩成7z? a. 代码的复制速度是非常慢的.  几M或几KB都是常事. b. 压缩成7z格式后,压缩速度迅速提高上百倍 网页为什么要压缩成7z? 怎么使用7z压缩(以好压2345为例子) ...

  5. 一段批处理脚本(for 嵌套)

    需求: 1.服务器上有一堆按日期生成的目录,已经有N个月了,需要只取当前月份的目录. 2.目录中有一系列文件,文件名字不一样,但存在一定的重复规律. 3.需要从服务器上拷贝文件到本地,自动去重,拷贝到 ...

  6. java基础解析系列(十)---ArrayList和LinkedList源码及使用分析

    java基础解析系列(十)---ArrayList和LinkedList源码及使用分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder jav ...

  7. 使用angular4和asp.net core 2 web api做个练习项目(三)

    第一部分: http://www.cnblogs.com/cgzl/p/7755801.html 第二部分: http://www.cnblogs.com/cgzl/p/7763397.html 后台 ...

  8. Appium python自动化测试系列之混合app实战(十一)

    12.1 什么是混合App 12.1.1 混合app定义 什么是混合app,其实这个不言而喻,我们的app正常来说应该都是native的,但是实际工作中却不是,反正种种原因我们的app会有native ...

  9. MUI点击事件获取当前对象,及当前对象的属性值

    //用惯了jquery,开始用mui还是有些不习惯 //直接贴代码吧 <nav class="mui-bar mui-bar-tab"> <a class=&qu ...

  10. CSharpGL(45)自制控件的思路

    CSharpGL(45)自制控件的思路 +BIT祝威+悄悄在此留下版了个权的信息说: 本文介绍CSharpGL实现自制控件的方法. 所谓自制控件,就是用纯OpenGL模仿WinForm里的Button ...