天天看點

挑戰:微服務內建測試挑戰:微服務內建測試Consumer-Driven Contracts:合約測試架構Pact我們的例子:

原文連結 https://codefresh.io/docker-tutorial/how-to-test-microservice-integration-with-pact/

挑戰:微服務內建測試

遷移到微服務對測試我們的系統産生了新的挑戰。理論上每個微服務都應該是隔離的并可以獨立操作。但在實踐中一個服務如果沒有其他部分通常沒什麼用。另一方面 - 為一個服務拉起整個系統的拓撲進行測試抵消了微服務期望帶來的子產品化和封裝。

挑戰在于如何檢驗與其他服務內建後沒有問題。我們希望越早越好。而且我們不想将複雜的生産環境重制一遍。一般來說這種檢驗是內建功能測試或叫端到端測試。但實際是當我們的系統越來越複雜 - 端到端帶來的收益越少。 大量的互相依賴導緻誤報和很長的執行周期。 使得測試變得很難管理與調試。

這甚至有一個測試金字塔理論(最初由Mike Cohn在他的著作‘Succeeding with Agile’中提到)講述了為了優化你的投入,你需要更少的高層次的端到端測試,寫更多的低層次的單元測試。

請閱讀本文并看看Codefresh(

https://codefresh.io/codefresh-signup/?utm_source=Blog&utm_medium=Post&utm_campaign=pactT)

, 他是對于Docker最好的CI。

挑戰:微服務內建測試挑戰:微服務內建測試Consumer-Driven Contracts:合約測試架構Pact我們的例子:

單元測試很好!但在它帶來的所有收益中 - 他們對測試與其他服務的內建沒什麼作用。

那我們怎樣保證每個服務團隊可以獨立的疊代但又能保證整體系統的健康呢?我們如何實作持續傳遞,小批量生産,快速回報,而又不會在每次變更時引起服務出問題呢?

一個可能的答案是Consumer-Driven Contract(CDC) 測試。這種測試政策是基于一種多年前就定義的服務進化模式。它現在分布式系統變得更常見後變得更适合了。

Consumer-Driven Contracts:

我嘗試簡單解釋一下。 Consumer-Driven Contracts實際就是面向服務與服務關系的合約。意思就是不想以前是provider提供方定義接口與服務級别是什麼樣(同僚消費者consumer盡量适配) - 現在消費者來領舞。 每個消費者來定義它期望服務提供方需要傳遞與需要檢查的。這就将內建的責任轉移到服務提供方。

那就變成以下流程:

挑戰:微服務內建測試挑戰:微服務內建測試Consumer-Driven Contracts:合約測試架構Pact我們的例子:

在商務合約上者通常描述成‘将消費者放在第一位’ 或‘傾聽你的客戶’。因為想要提供最好的服務我們需要盡量做到客戶期望和需要的。而不是我們假設對的事。

當讨論微服務進化時 - 在那種每個服務都有一個獨立團隊開發的大型企業裡尤其重要。有時這些團隊也可能在不同的地理位置和區域。這影響了即時溝通和讓業務功能進化更有挑戰性。

合約測試架構

消費者驅動合約當然可以通過投資團隊間的溝通與協作來管理。 也可以通過使用結構化的系列化格式如protobuf,thrift或messagepack消息體來解決。但如果要管理一個定義好的流程 - 最好使用架構,尤其如果是個開源的。

這種架構已經出現了。這其中最傑出和活躍的是Pact和Spring Cloud Contract。後者隻針對使用JVM的項目。 而Pact使用Ruby寫的但可以支援很多語言,包括Java,Go,Python,Javascript。 讓它很适合在複雜,多樣性的微服務系統中使用。

今天我們會看看如何在兩個服務間定義和校驗合約。消費者服務是用Python寫的。而提供方服務是用Go寫的。測試會在我們的CI/CD流程中進行 - 也就是在Codefresh流水線裡面。

Pact

是以,Pact怎麼工作的?它開始于消費者。

消費者服務的開發寫一個測試。測試定義了與提供方的內建。這包括了提供方需要的狀态,請求的消息體和期望的結果。基于這個定義Pact建立和運作一個提供方的樁來進行測試。這個測試的輸出回事一個或多個json檔案,一般是這樣的:

{
  "consumer": {
    "name": "billy"
  },
  "provider": {
    "name": "bobby"
  },
  "interactions": [
    {
      "description": "My test",
      "providerState": "User billy exists",
      "request": {
        "method": "POST",
        "path": "/users/login",
        "headers": {
          "Content-Type": "application/json",
        },
        "body": {
          "username":"billy",
          "password":"issilly"
        }
      },
      "response": {
        "status": 200,
      }
    },
  ],
  "metadata": {
    "pactSpecification": {
      "version": "2.0.0"
    }
  }
}           

這就是合約,這就是pact。 現在他們被傳給服務提供方。也可以被送出給共享的git倉庫,或通過Pact Broker應用上傳到共享的檔案存儲。

一旦合約更新過了 - 提供方需要對其進行測試驗證是否仍符合要求。它通過使用共享的pact檔案運作它自己的校驗測試而不是真實版本的服務。如果所有的互動是符合預期的并測試通過了 - 我們就可以繼續了。 如果不 - 提供方的開發需要通知消費方的開發。然後,他們可以一起分析什麼導緻了合約的失敗。

我們的例子:

我們會測試2個小服務的內建。

服務provider是我們在jenkins plugin例子裡使用過的相同的服務。它叫’bringon‘,是用Go寫的一個是用mongoDB的儲存軟體建構資訊的系統資料庫。

我們的consumer是一個很薄的python用戶端,它隻知道從bringon是用建構編号來擷取建構資訊的。

在CDC裡consumer先開始 - 我們從這來。

consumer代碼現在由一個帶有2個函數的client.py檔案組成。我們隻關注叫’build‘的函數 - 它是我們要測試的函數。

import requests

…

def getbuild(host, port, buildnum):
    """Fetch a build by number ."""
    uri = 'http://' + host + ':' + port + '/builds/' + str(buildnum)
    return requests.get(uri).json()           

為了為它生成pact - 我們寫一個叫build_test.py的測試檔案:

import atexit
import unittest
import client

from pact import Consumer, Provider

pact = Consumer('buildReader').has_pact_with(Provider('bringon'))
pact.start_service()
atexit.register(pact.stop_service)

class GetBuildInfoContract(unittest.TestCase):
  def test_get_build(self):
    true = True
    expected = {
      u'name':u'#3455',
      u'completed': true, #boolean
      u'info':{
        u'coverage':30,
        u'apiversion':0.1,
        u'swaggerlink':u'http://swagger',
        u'buildtime':230}
    }

    (pact
     .given('build 3455 exists')
     .upon_receiving('a request for build 3455')
     .with_request('get', '/builds/3455')
     .will_respond_with(200, body=expected))

    with pact:
      result = client.build(3455)

    self.assertEqual(result, expected)           

這很直接 - 我們建了一個mock的service,定義了一個期望的http reponse和body, 并調用client.build()來保證互動是按期望進行的。

如果一切正常 - 一個叫buildreader-bringon.json的pact檔案會寫入到我們的工作目錄。

現在我們可以将這個檔案發給bringon的開發,讓他們可以用這個pact來測試他們的服務。

這可以用pact-go來完成 - Golang的架構。測試看起來會是這樣:

func TestPact(t *testing.T) {
    go startInstrumentedBringon()
    pact := createPact()
    // Verify the Provider with local Pact Files
    log.Println("Start verify ", []string{filepath.ToSlash(fmt.Sprintf("%s/buildreader-bringon.json", pactDir))},
        fmt.Sprintf("http://localhost:%d/setup", port), fmt.Sprintf("http://localhost:%d", port), fmt.Sprintf("%s/buildReader-bringon.json", pactDir))
    err := pact.VerifyProvider(types.VerifyRequest{
        ProviderBaseURL:        fmt.Sprintf("http://localhost:%d", port),
        PactURLs:               []string{filepath.ToSlash(fmt.Sprintf("%s/buildReader-bringon.json", pactDir))},
        ProviderStatesSetupURL: fmt.Sprintf("http://localhost:%d/setup", port),
    })

    if err != nil {
        t.Fatal("Error:", err)
    }

}           

記住這需要一點額外的工作。我們需要需要實作startInstrumentedBringon()方法,它使用額外的'/setup' endpoint來定義服務狀态并啟動我們的服務。在我們的場景這用于建立一個入口點來滿足我們消費者的期望。我們也需要建立一個Pact用戶端對象來校驗所有互動動作。像這樣:

func createPact() dsl.Pact {
    // Create Pact connecting to local Daemon
    log.Println("Creating pact")
    return dsl.Pact{
        Consumer: "buildreader",
        Provider: "bringon",
        LogDir:   logDir,
        PactDir:  pactDir,
    }
}           

一個使用pact-go的缺點是需要你在後端運作一個常駐程序。這個程序控制服務的初始化,關閉和pact校驗。

這在容器内運作一個獨立臨時程序不太合适。

是以如果我們需要的是用pact測試我們的服務 - 我們可以使用pact-go包中的輕量級pact-provider-verifier工具。

像這樣:

pact-provider-verifier --pact-urls <path_to>/buildreader-bringon.json --provider-base-url http://localhost:8091 --provider-states-setup-url http://localhost:8091/setup           

請記住在這個例子裡我們需要實作和建構'/setup' endpoint作為我們服務的一部分。這在我們想讓我們的服務可測試時是個好主意。

服務的代碼可以在我們的Github找到:

bringon(也就是Provider):

https://github.com/codefreshdemo/bringon

buildreader(也就是消費者)

https://github.com/antweiss/cdc-pact-demo

Pact源碼和例子:

https://github.com/pact-foundation

在這個部落格的下篇我們會展示如何讓運作合約測試作為你的Codefresh(譯者注:codefresh.io是一個CI/CD提供商) 流水線的一部分。