契約測試框架-Pact實踐


在前一篇博客中我們講到契約測試是什么,以及它能給我們軟件交付帶來什么價值,本次將介紹一個開源的契約測試框架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 = 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之后我們再跑一遍測試,結果顯示如下:

運行結果會顯示實際返回的和期望的差異,這就達到了契約測試的目的。

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM