背景
在現代的開發模式中,基于微服務的開發模式越來越常見,但是隨著項目規模的擴大,服務與服務之間的依賴越來越密切,當不同的開發團隊去開發不同的服務時,服務的提供者的變動會影響到眾多消費它的消費者,為了保證系統的正確性和一致性,這將需要大量的溝通成本和代碼修改的時間成本。
之前遇到的某個客戶內部就是因為服務與服務之間依賴過多,且存在各種的物理依賴,再加上其他種種原因,使得在集成測試時bug激增。對于他們而言集成測試需要依賴于各個服務版本的一致性以及真實的物理環境,因此他們的集成測試通常需要用上幾個小時才可以完成,這就使得整體的效率大大縮減。除此之外,在集成測試中發現的問題也會使得他們花很長的時間去定位到問題所在。
相似的問題在平時的開發過程中也是經常遇到,由于依賴方的接口變更導致在系統集成時頻頻出錯,整體的代碼又不得不再加修改,這就使得開發的進度遲遲無法向前推進。
為了解決這類的問題,契約測試應運而生。契約測試不是一個新鮮東西,但在實際項目經歷中發現用好契約測試真的會大大增強開發的效率,因此寫下這篇文章來簡單總結一下契約測試的一些內容。
首先什么是契約測試
契約測試是一個為確保兩個獨立的系統或者微服務能夠兼容并可以相互通信的一個方法,契約測試分為兩種,一種是服務提供者驅動的,另一種是消費者驅動的。如下圖所示,左側是一個服務的消費者,右側是一個服務提供者,消費者調用提供者的接口并消費數據的交互過程會被記錄成一份契約,在契約中包含了服務的提供者和消費者是誰,以及消費者對服務的提供者的期望(如請求的參數和返回的結果)。服務的提供者會根據這份契約去反復驗證自己是否能夠滿足消費者的需求,這也就是所謂的消費者驅動。
契約測試主要是為了驗證服務層提供的數據是否能夠消費者正常使用,它不會深入去測試服務的行為,而只是專注于測試服務的輸入與輸出,因此相比于沉重的集成測試而言,契約測試會更加的輕巧,快速。契約測試形式上類似于API級別的UT,但其本質上還是個集成測試,比API測試在金字塔的位置更靠頂端,所以容易導致契約測試的數量增加和不穩定性增加。
契約測試具體是如何實踐的
接下來我們分別從代碼和流水線設計兩方面來闡述一下具體的契約測試的實踐:
代碼層面:
為了完成契約測試,我們可以借助一個叫pact的工具。pact是一個代碼優先的用來支持契約測試的一個工具,它目前支持java,python,go等主流的開發語言。
Pact中的一些基本概念:
- Contract: 契約文件,在Pact中也叫做pact,可以保存在本地,也可存在broker中?
- Provider: 真正運行的生產者服務?
- Consumer: 接收生產者發出的數據?
在pact中,consumer和provider分別做了不同的事:
Consumer端:
consumer端會做這么幾件事:
- 首先使用pact dsl定義它消費的接口的request和response,并注冊到mock server中?
- 然后consumer端的測試會發送一個真實的請求到pact起的一個本地的mock server?
- 接著pact會去對比實際的request和expected request 是否一致,如果一致則返回expected response?
- 最后consumer會去確認這個返回值是否正確 上面所有步驟都pass后,整個的consumer測的pact測試才算結束,此時consumer定下的契約會被發布到一個叫pact broker的地方進行契約的統一管理。?
Pact broker是pact提供的一個專門用來統一管理契約的一個服務,在這個服務中,開發者們可以清晰的看到所有的服務提供者和消費者的詳細信息。
總的來說,cousumer端的主要功能是生成契約(文件的載體),驗證request和response的工作是可選的,借由consumer端的集成測試的形式,確保生成的契約的確是consumer真正期望的,通俗來講,就是“測試測試的測試”。
Provider端:
在provider端,pact會mock出一個consumer并發送請求給provider端真實運行著的進程,provider在接受到請求后會根據自己的代碼實現將真實的response返回給pact,接著pact會拿著這個response去和pact broker上獲取到之前consumer定義的契約并進行比對,如果provider能夠滿足契約,則驗證通過。
當consumer和provider的測試都通過后,產品則就可以被部署到指定環境了。
以上是消費者驅動的一個實踐方式,消費者驅動的契約測試主要適用于以下場景:
- 消費者和提供者都是可控的?
- 消費者的需求變動能夠變成提供者的需求?
- 消費者數量不是很多,作為提供方能夠管理的過來?
符合以上的條件的場景下,比較適合使用消費者驅動的契約測試。消費者驅動的背景下,服務提供方可以基于消費者提出的契約快速做出反饋。
然而,在實際的情況可能不是這么美好,之前遇到的客戶,他們內部的部分情況恰恰違背了以上的場景。他們的產品極度依賴著一些外部的底層依賴,且底層的依賴變動頻率較高,這使得他們會頻頻的在集成測試時發現底層已經發生了變動。在這種情景下,提供者驅動的契約測試更加適合。由服務的提供方來約定契約,然后眾多的消費者去滿足契約,當提供方發生變動時,消費方能夠及時感知到并快速反饋。整體的實踐流程只需將上方的consumer者和provider的操作進行轉置即可。
換句話說,消費者驅動和提供者驅動的區別在于誰去響應契約的變化。就如上方提到的,外部的提供者依賴是不可控的情況下,提供者驅動的模式會更加合適,相反則是消費者驅動的模式。
流水線的設計
當選擇消費者驅動的契約測試策略時,作為一個consumer,它要做的就是去發布契約,告訴provider它的需求。那么作為provider,它就需要去檢查自己的實現是否能夠滿足consumer的需求,那么當它的實現無法滿足契約時,則此時的流水線契約測試階段就應該顯示fail,并告知對應的provider,讓其快速做出修正 。如圖所示,當consumer發布了新版本的契約,這將導致provider端的流水線fail,那么此時provider就會得知他們需要根據新的契約來修改實現了。
而和消費者驅動相反,提供者驅動的設計則是當provider發布了一個新的契約之后consumer側的流水線會變紅,直到consumer將他們的代碼根據新的契約修正后才可以進入后面的集成測試。
契約測試帶來的好處
(1) 測試的速度快,無需依賴多個系統之間的交互
細心的同學通過上面的描述會發現,在契約測試時服務的依賴方式不需要被真實調用的,契約測試通過mock依賴的方式來模擬依賴方的行為,這就使得測試的速度得以大大提升
(2) 可以并行開發
由于mock的存在,使得服務的消費方和提供方可以根據事先定義好的契約進行并行開發
(3) 發現問題后可以快速定位到問題:
因為問題只會出現在當前測試的服務或者組件中,你甚至可以確切的知道是哪個api測試fail了
(4) 在確定完契約之后,開發人員可以在本地就可以進行測試,無需將代碼推至遠端
(5) 測試前移
把本來要通過集成測試才能驗證的工作化作單元測試和接口測試,用更輕量的方式快速進行驗證,更早的發現問題使得后續的測試更加快速
契約測試和其他測試的對比
總結
總體來說,契約測試是一個介于單元測試和集成測試的一個階段,他關注的細粒度比單元測試更粗,但是又無法取代集成測試。尤其是當你的產品對環境依賴特別大的時候,集成測試還是必不可少的一部分,契約測試的存在只是為了讓你在開發過程中的聯調更加快速,集成時問題更少。?