在前一篇博客中咱們講到契約測試是什麼,以及它能給咱們軟件交付帶來什麼價值,本次將介紹一個開源的契約測試框架Pact,它最初是用ruby語言實現的,後來被js,C#,java,go,python 等語言重寫,此文將介紹Pact框架的相關知識並結合示例代碼講解在實際項目中應該怎麼使用。
javascript
Pact是一個開源框架,最先是由澳洲最大的房地產信息提供商REA Group的開發者及諮詢師們共同創造。REA Group的開發團隊很早便在項目中使用了微服務架構,並在團隊中對於敏捷和測試的重要性早已造成共識,所以設計出這樣的優秀框架並應用於平常工做中也是十分天然。html
Pact工具於2013年開始開源,發展到今天已然造成了一個小的生態圈,包括各類語言(Ruby/Java/.NET/JavaScript/Go/Scala/Groovy...)下的Pact實現,契約文件共享工具Pact Broker等。Pact的用戶已經遍佈包括RedHat、IBM、Accenture等在內的若干知名公司,Pact已是事實上的契約測試方面的業界標準。前端
Pact是支持消費者驅動的契約測試框架,針對微服務的模式下多個單獨服務的接口契約測試以及先後端分離的模式提供了很好的支持。java
消費者端做爲數據的最終使用者很是清楚,明確的知道須要的什麼樣格式,類型的數據,它將負責建立契約文檔(包含結構和格式的json文件),服務提供端將根據消費者端建立的契約文檔提供對應格式的數據並返回給消費者,經過契約檢查判斷若是服務端提供的數據和消費者生成的契約不匹配,將拋出異常並提示給服務端。總結以下:python
服務消費者git
服務消費者是指向另外一組件(服務提供者
)發起HTTP請求的組件。注意這並不依賴於數據的發送方式——不管是GET
仍是PUT
/ POST
/ PATCH
,消費者
都是HTTP請求的發起者。github
服務提供者正則表達式
服務提供者是指向另外一組件(服務消費者)的HTTP請求提供響應的服務器。json
模擬服務提供者 模擬服務提供者用於在消費者
項目中的單元測試裏模擬真實的服務提供者
,意味着沒必要須要真實的服務提供者
就緒,就能夠將類集成測試運行起來。後端
Pact文件
Pact文件是指一個含有消費者
測試中所定義的請求和響應被序列化後的JSON的文件,即契約。
要對一個Pact
進行驗證,就要對Pact
文件中所包含的請求基於提供者
代碼進行重放,而後檢查返回的響應,確保其與Pact
文件中所指望響應相匹配。
提供者狀態
在對提供者重放某個給定的請求時,一個用於描述此時提供者
應具備的「狀態」(相似於夾具)的名字——好比「when user ken does not exists」或「when user ken has a bank account」。
提供者
狀態的名字是在寫消費者
測試時被指定的,以後當運行提供者
的pact驗證時,這個名字將被用於惟一標識在請求執行前應運行的代碼塊。
當你的團隊同時負責開發服務消費者與服務提供者,而且服務消費者的需求被用來驅動服務提供者的功能時,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 = 12, FirstName = "Ken", LastName = "Wang", Users = new List<User>() { new User() { Name = "asd", Age = "1" }, new User() { Name = "asd", Age = "1" }, new User() { Name = "kenwang", Age = "223", 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以後咱們再跑一遍測試,結果顯示以下:
運行結果會顯示實際返回的和指望的差別,這就達到了契約測試的目的。
咱們能夠看到生成的contract文件中有matchingRules 的節點,這個節點下面就是爲了添加匹配規則的,目前支持四種匹配方式:
正則匹配:
將執行正則表達式匹配值的字符串表示
類型匹配:
將根據值執行一個類型的匹配,也就是說,若是它們是相同的類型,則它們是相等的
元素最小長度匹配:
根據值執行一個類型的匹配,也就是說,若是它們是相同的類型,則它們是相等的。此外,若是值表示集合,則實際值的長度與最小值進行比較。
集合最大長度匹配:
根據值執行一個類型的匹配,也就是說,若是它們是相同的類型,則它們是相等的。此外,若是值表示集合,則實際值的長度與最大值進行比較。
類型匹配只適用於一些簡單類型的匹配,負責類型,如郵箱等須要用正則來匹配。
內容就介紹到這裏,若是你們有更好的經驗,歡迎分享交流。
學習參考:
https://github.com/pact-foundation/pact-net.git