1. 程式人生 > >提升微服務測試效率:消費者驅動契約測試

提升微服務測試效率:消費者驅動契約測試

本文由公眾號EAWorld編譯發表,轉載需註明出處。


作者:EAWorld
編譯:白小白 

全文4977字,閱讀約需要8分鐘
 

概述:


在軟體工程的世界裡,我們經常面臨變化。微服務不僅改變了軟體的體系結構,而且改變了團隊的組織方式和協作方式。

相對於單體式應用,微服務有其優勢,同時,也有引入後所新產生的問題,測試就是問題之一。

在這篇文章中,我們想概述一下測試如何在微服務的新世界中發生變化。我們還將介紹消費者驅動的契約測試的細節和支援它的框架。

為了較為全面的闡述CDCT的概念,本文翻譯、引用、和綜合了多篇相關文章的內容,相關連結附後。
 

目錄:


一、單元測試

二、端到端(系統)測試

三、整合測試

四、使用消費者驅動契約測試(CDCT)

五、總結
 

一、單元測試


當我們談到微服務時,我們還應該進行單元測試嗎?答案是肯定的,單元測試已經證明是一種可靠的、快速的,也不是那麼昂貴的方法來測試業務邏輯的有效性。但是單元測試僅僅保證服務提供者或者服務消費者某一方的程式碼是有效的或者功能是正常的,而不能保證服務之間的互動是有效的。而這恰恰是微服務的核心應用場景之一。
 

二、端到端(系統)測試


當我們談到微服務時,我們還應該進行端到端的測試嗎?是的,進行端到端測試是很重要的,但是當我們談到微服務時,為了執行端到端的測試,需要部署從服務消費者到服務提供者之間所有環節的相關呼叫,複雜程度可能會非常高。
 

三、整合測試


測試兩個服務(提供者和消費者)之間的互動的傳統方法是使用整合測試。這樣做的目的是在某些整合環境中同時執行消費者服務和提供者服務,並檢查它們是否按預期進行互動。這種型別的測試模擬了服務在生產環境中的行為,因此在理論上整合測試是有意義的。然而,這種方法存在一些問題。

首先,整合測試通常比較慢。它們需要設定整合環境,啟動消費者和提供者服務並初始化它們的依賴關係。起初,這似乎不是一個問題,但是隨著整合測試的數量開始增加,構建過程變得越來越慢。在微服務體系結構中尤其如此。在每一對互動的微服務之間進行整合測試是不合適的。

整合測試的另一個問題是它們很脆弱。有時,它們會因為與服務本身無關的原因而失敗,可能存在網路問題或資料庫之類的外部依賴關係。而意味著失敗的整合測試並不一定意味著程式碼存在問題。

整合測試的另一個問題是定位困難。即使由於消費者和提供者服務之間的實際整合問題而導致整合測試失敗,很難確定問題的所在:這是消費者服務的錯誤嗎?還是提供者的服務?還是兩者兼而有之?

整合測試增加了額外的團隊開銷。整合測試主要由QA團隊執行,而不是由開發人員自己執行,這意味著在出現問題時,團隊之間需要額外的開銷。這也導致了一個問題:誰更適合測試兩個服務之間的整合點:QA團隊?還是服務的實際開發人員?在到達QA之前,清楚地知道兩個服務在開發時是否正確地互動,將為我們節省大量的時間和開銷。
 

四、使用消費者驅動契約測試(CDCT)


雖然三種方式各有利弊,但與整合測試及端到端測試相比,單元測試相對來說是健壯、可靠的,它們工作速度快,並且非常具體地告訴我們問題在哪裡。如果可以更加有效的測試方法改進單元測試來驗證服務間互動,肯定會改善我們的開發、測試和部署體驗。

消費者驅動契約測試(Consumer-Driven Contracts Testing)背後的理念是定義每個服務消費者與提供者之間的契約,然後根據該契約對消費者和提供者進行獨立測試,以驗證他們是否符合契約約定的事項。

為了更好地理解,我們將使用以下示例模型來描述這一微服務測試方法背後的概念。



在上圖中,我們可以看到兩個微服務通過REST相互通訊。第一個服務是消費者(Consumer)的角色,第二個是提供者(Provider)的角色。

當服務提供者不發生變化的情況下,比如我們通過Mock模擬服務提供者的相關反饋,相關測試是可以通過的。



但是,如果是在生產環境中,測試時模擬的服務反饋很可能跟不上服務提供者的變化,比如服務提供者更改了服務的資料格式,從“名字,姓名“到”人名“。整合測試將無法捕捉到這個問題,因為它們是針對過時版本的提供程式執行的,此時,就會發生如下的情況。



消費者驅動契約的理念是將服務消費者和提供者之間的互動正式化。服務消費者建立一個契約,它是服務消費者和提供者之間就他們之間將要發生的互動達成的協議。或者換句話說,提出服務消費者對提供者的期望。一旦提供者就契約達成協議,消費者和提供者都可以獲取契約的副本,並使用測試來驗證它們的相應實現沒有違反契約。



消費者驅動的契約測試,通常實現方式如下:

1. 選擇合適的場景,定義消費者的請求和期望的響應。

2. 使用Mock機制,為消費者提供模擬的提供者以及期望的響應。

3. 記錄消費者傳送的請求、提供者提供的響應以及關於場景的其它元資料,並將其記錄為當前場景的契約。

4. 模擬消費者,向真正的提供者模擬傳送請求。

5. 驗證提供者提供的契約是否和之前記錄的契約一樣。

這種新的測試方法的優點是它們基本上是添加了互動條件的單元測試:它們可以在本地獨立執行,而且速度快、可靠。但這其實與Mock方式模擬的好處相當,事實上,CDCT所帶來的優勢遠非如此。

優勢1:降低介面變化帶來的服務消費者風險

CDCT契約的發起方是服務消費者,由服務消費者定義自己所需要的反饋資訊,因此,可以保證服務消費者總是能夠獲得自己所需的反饋。而不論服務提供者一方發生了什麼變化。以CDCT測試框架PACT為例。



服務消費者通過建立模擬提供者的Mock,可以對請求、響應和相關資訊記錄下來,成為一個Pact檔案。這個檔案就是消費者與提供者之間的契約。在這個過程中,服務提供者無需進行任何操作。



接下來,在服務提供者一端,將通過模擬消費者的Mock對Pact檔案進行回放,要求服務提供者針對該契約做出正確的響應。通過這樣的的過程,完成一次完整的從服務消費者向服務提供者的驅動過程。



當服務提供者需要對介面做出變更時,仍舊需要遵循契約的要求,以反饋正確的結果,這樣,就可以保證服務消費者總是得到正確的資訊而不論服務提供者的介面發生怎樣的變化。除非消費者端主動的重新訂立契約。

優勢2:解耦開發團隊,降低測試成本,解放生產力

當服務消費者和服務提供者以契約為中介形成解耦的時候,相關的技術團隊也因此形成了解耦,而不需要一定針對一個端到端的測試場景來進行配合。

這裡我們引入兩個技術團隊進行相關的測試。左側的是服務消費者,需要通過ID查詢使用者的郵件地址,右側的是服務提供者,負責反饋正確的郵件地址資訊。



在服務消費者和提供者之間建立一個契約,我們稱之為TEST,來要求服務提供者根據ID反饋正確的EMAIL。



服務消費者可以通過執行TEST測試來了解自己能否獲得正確的資訊,但事實上,這並沒有必要,因為只有當服務提供者一方發生服務介面的變更時,才會影響契約的效力,所以正確的做法是,只需要在服務提供者一方來進行對契約的驗證測試即可。



這樣,服務消費者將通過契約來驅動服務提供者完成既定的功能反饋,當雙方對此過程協調一致,運轉正常之後,服務提供者將不再需要服務消費者來發布任何契約的變更,就可以單獨的依賴契約發現程式碼的缺陷。而服務消費者技術團隊,就可以專注於本身的事情,甚至於去支援其他的專案內容。



應用場景舉例:第三方API的整合測試

在現實場景中,一方面企業內部會有諸多的遺留系統API,另一方面也同時會有很多情況需要呼叫外部的API,比如谷歌地圖,這些情況下API並不受我們的掌控,即使提交一些反饋,相關變更也可能以數週或者數月為單位,甚至對於遺留系統來說,相關的供應商都已經不復存在。

對於應用將對這類API進行整合的場景,此時,應用是消費者端,而API是服務提供端,我們可以有三種處理方式:

1、消費者端手動檢查:通過手動檢查應用程式是否做了它應該做的事情以及是否使用了來自API的正確值來確保應用程式仍然工作。

2、服務者端真實呼叫:首先確認API被正確整合,測試的時候直接呼叫API來檢查相關功能是否正確,這將涉及網路帶來的測試速度的影響,以及呼叫費用的消耗,畢竟每一次呼叫都不是免費的。

3、記錄服務端反饋,並在程式碼庫中回放:在這種情況下,僅需要呼叫一次API,並將相關反饋記錄為JSON檔案,從而解決了網路和費用問題,但仍舊無法繞開一旦服務介面發生變化帶來的影響。

引入 CDCT可以緩解這個問題。但顯然我們不能將契約釋出給Google Maps API或我們遺留的CRM系統,並迫使他們遵守。這些提供者可能既不關心也不具備支援CDCT的工具。因此,乍一看,為第三方API使用CDCT似乎很奇怪。

我們可以做的是在自動化測試期間,建立另一個服務,作為谷歌API的替代品。該服務將儲存從實際API中定義所需欄位的契約。我們稱這些服務為代理。它們從不代理HTTP請求,而是在自動化測試期間充當谷歌API和應用之間的中間角色。代理將有兩個目標:

1.確保API按預期響應,就像在實際呼叫真實的谷歌API一樣。

2.向服務消費者提供契約檔案,以供回放,類似於一個JSON響應檔案。

讓我們舉個例子,我們要展示從德國斯圖加特到柏林需要多長時間。使用Google距離矩陣API 我們進行如下的呼叫:

http https://maps.googleapis.com/maps/api/distancematrix/json \
origins==Berlin destinations==Stuttgart


呼叫結果是

{
"destination_addresses" : [ "Berlin, Germany" ],
"origin_addresses" : [ "Stuttgart, Germany" ],
"rows" : [
{
"elements" : [
{
"distance" : {
"text" : "636 km",
"value" : 635736
},
"duration" : {
"text" : "6 hours 18 mins",
"value" : 22651
},
"status" : "OK"
}
]

},
"status" : "OK" 


通過這樣的請求呼叫,我們瞭解到,從柏林開車到斯圖加特需要大約6小時18分鐘的時間。這個時間是通過22651來換算取得。這是以秒為單位的持續時間。我們的服務消費者,例如Android應用程式,可能想決定他們想如何為使用者對這個值做格式化。因此我們應該確保這個經行時間欄位包含在響應中,也就是說,針對這個值做契約上的約定。

以 Spring Cloud Contract 的 Groovy DSL 為例,我們可以定義如下的契約:

org.springframework.cloud.contract.spec.Contract.make {
request {
method GET()
url("/maps/api/distancematrix/json") {
queryParameters {
parameter 'origins': 'Berlin'
parameter 'destinations': 'Stuttgart'
}
}
}
response {
status 200
body([
rows  : [[
elements: [[
duration: [
value: 22651
]
]]
]],
])
}
}

可以看到,相對於此前的完整反饋,契約只包含我們關心的部分響應和用於建立預期響應的所應發出的請求。框架將可以自動生成以下的測試程式碼和相關欄位 的斷言。

@Test
public void validate_shouldProvideDistanceBetweenTwoCities() {
// when:
Response response = webTarget
.path("/maps/api/distancematrix/json")
.queryParam("origins", "Berlin")
.queryParam("destinations", "Stuttgart")
.request()
.method("GET");
String responseAsString = response.readEntity(String.class);
// then:
assertThat(response.getStatus()).isEqualTo(200); 
// and:
DocumentContext parsedJson = JsonPath.parse(responseAsString);
assertThatJson(parsedJson).array("['rows']")
.array("['elements']").field("['duration']")
.field("['value']").isEqualTo(22651);


如果我們在請求中提供指定的引數(when部分),預期應該獲得指定的響應(then部分)。生成的契約測試不需要我們編寫任何實現程式碼就可以通過。

並且在測試執行之後,我們會得到一些JSON檔案作為存根,類似PACT的契約檔案,儲存在本地用於應用測試。

如果實際的谷歌API服務調整了兩地的行經時間由25561改為25562,上述的程式碼可能就並不適用了。我們需要將生成的斷言修改如下的內容:

assertThatJson(parsedJson).array("['rows']")
.array("['elements']").field("['duration']")
.field("['value']").matches("\\d+");

這樣,在實際呼叫過程中,即使谷歌API反饋的是12345,服務消費方也不會崩潰。

此外要讓測試命中存根而不是真正的API,我們需要配置如下的服務對映。

stubrunner:
ids: 'co.hodler:scdcproxy:+:stubs'
stubsMode: LOCAL
ids-to-service-ids:
scdcproxy: google-distance-service

通過使用CDCT技術,我們確保了

該API的行為與我們預期的一樣。

除了代理專案之外,我們的測試不呼叫真正的API。

我們確保預期的響應和實際的響應之間沒有不匹配。

主流框架介紹

能夠完成CDCT任務的框架有Janus\Pact\Pacto\Spring Cloud Contract等,網上可以找到比較多資料的是PACT和Spring Cloud Contract。

PACT(https://docs.pact.io/)

其官網的說明是這樣的:

PACT是一種契約測試工具。契約測試是一種確保服務(例如API提供程式和客戶端)能夠相互通訊的方法。如果沒有契約測試,瞭解服務可以通訊的唯一方法就是使用昂貴而脆弱的整合測試。你是否放火燒了你的房子來測試你的煙霧報警器?不,你用測試按鈕來測試它和你耳朵之間的合同。PACT為您的程式碼提供了測試按鈕,允許您安全地確認您的應用程式將一起工作,而不必先部署這個世界。

Pact是一個開源框架,最早是由澳洲最大的房地產資訊提供商REA Group的開發者及諮詢師們共同創造。REA Group的開發團隊很早便在專案中使用了微服務架構,並在團隊中對於敏捷和測試的重要性早已形成共識,因此設計出這樣的優秀框架並應用於日常工作中也是十分自然。

Pact工具於2013年開始開源,發展到今天已然形成了一個小的生態圈,包括各種語言(Ruby/Java/.NET/JavaScript/Go/Scala/Groovy...)下的Pact實現,契約檔案共享工具Pact Broker等。Pact的使用者已經遍及包括RedHat、IBM、Accenture等在內的若干知名公司,Pact已經是事實上的契約測試方面的業界標準。

Spring Cloud Contract(https://cloud.spring.io/spring-cloud-contract/)

Spring Cloud Contract是一套完整的解決方案,幫助使用者成功地實現消費者驅動的契約方法。目前,Spring Cloud Contract的主體是Spring Cloud Contract Verifier專案。

Spring Cloud Contract Verifier是一個工具,它支援基於JVM的應用程式的消費者驅動契約(CDC)開發。用Groovy或YAML編寫契約定義語言(DSL)。

Spring Cloud Contract Verifier將TDD提升到軟體體系結構的級別。
 

五、總結


消費者驅動的契約測試,關鍵理念在於兩個方面:

一是,通過提供中介契約,形成了服務消費者和服務提供者之間的解耦

二是,由消費者出發釋出契約的方式,確保服務消費者的價值得以優先實現

從而帶來的好處是:

一是服務提供端的介面變化不會對服務消費端產生影響

二是降低了傳統的整合測試以及端到端測試過程中的昂貴成本。

三是快速反饋、獨立部署、降低複雜度,更快的開發速度和更短的迭代時間。


本文直接引用或者參考瞭如下的文章來源:
1.https://blog.csdn.net/wzxq123/article/details/80219772

2.https://techbeacon.com/end-end-vs-contract-based-testing-how-choose

3.https://techblog.poppulo.com/why-should-you-use-consumer-driven-contracts-for-microservices-integration-tests/

4.https://dzone.com/articles/consumer-driven-contracts-with-pact-feign-and-spri

5.http://www.lor.beer/a-guide-to-testing-microservices/

6.http://www.lor.beer/how-to-consumer-driven-contract-tests/

7.http://hecodes.com/2016/10/better-testing-microservices-using-consumer-driven-contracts-node-js/

8.https://blog.novatec-gmbh.de/introduction-microservices-testing-consumer-driven-contract-testing-pact/

9.https://medium.com/@axelhodler/integration-tests-for-third-party-apis-dab67c52e352

10.https://www.cnblogs.com/Wolfmanlq/p/7966408.html


關於EAWorld:微服務,DevOps,資料治理,移動架構原創技術分享,長按二維碼關注