聊一聊契約測試

聊一聊契約測試

什麼是契約

如果從契約產生的階段來說,現有資料表明最早要追溯到西周時期的《周恭王三年裘衛典田契》,將契約文字刻寫在器皿上,就是為了使契文中規定的內容得到多方承認、信守,「萬年永寶用」。所以訂立契約的本身,就是為了要信守,就是對誠信關係的一種確立。誠信,是我國所固有的一種優良傳統,也是延續了幾千年的一種民族美德,在中國儒家的思想體系里,是倫理道德內容中的一部分。

《現藏於台北故宮博物院》

現實真的是那麼美好嗎?小時候的價值觀教育未能改變社會的現狀,缺少契約精神的案例卻比比皆是。

那麼,契約真的要消失了嗎?不盡然,在軟體測試領域,我們又重新拾起了契約這把利器。

發展歷程

接下來讓我們把時間回溯到2011年初,回到老馬的文章《集成契約測試》中來,回顧一下契約測試的起源和發展歷程:

假設我們有這樣一個場景:A團隊負責開發API服務,B團隊進行API調用消費服務。

為了保證API的正確性,我們會對外部系統的API進行測試(除非你100%相信外部系統永遠正確和保持不變),這很可能就會導致一個問題,當外部系統並不那麼穩定或者請求時間過長時,就會導致我們的測試效率很低,並且穩定性下降。比如當外部API掛掉導致測試失敗時,你並不能完全確信是API功能被更而改導致的失敗還是運行環境不穩定導致的請求失敗。

最初,解決這個問題的方案是構建測試替身(Test Double),通過模擬外部API的響應行為來增強測試的穩定性和反應速度。實現手段是在測試環境中搭建一個模擬服務環境,通過設定一些請求參數來返回不同的響應內容,然後再被內部系統調用,來保證調用端的正確性。構建模擬環境時我們可以使用幾種不同的測試手段,如Dummy,Fake,Stubs,Spies,Mocks等。可是,問題又來了,如果使用測試替身那如何能保證外部系統API變化時得到及時的響應,換句話說,當內部系統測試都通過的通過時,如何能保證真正的外部API沒有變化?

?

一個比較簡單的方式是部分測試使用測試替身,另外一部分測試定期調用真實的外部API,這樣既保證了測試的運行效率、調用端的準確性,又能確保當真實外部系統API改變時能得到反饋。

是不是到這裡就皆大歡喜了呢?

如果劇情到這裡就結束的話,未免太過俗套。這個方案最大的缺陷在於API的反應速度,真實外部API的反饋周期過長,如果減少真實API測試間隔時間就又會回到文章最開始的兩難境地。

那麼如何解決這個問題呢?先來讓我們剖析一下前面幾種解決方案的共通點。

在上面的場景中,我們都是已知外部API功能來編寫相應的功能測試,並且使用直接調用外部API的方式來達到驗證測試的目的,這樣就不可避免的帶來兩個問題:

第一,服務消費方對服務提供方API的更改是通過對API的測試來感知的。

第二,直接依賴於真實API的測試效果受限於API的穩定性和反映速度。

?

解決方式首先是依賴關係的解耦,去掉直接對外部API的依賴,而是內部和外部系統都依賴於一個雙方共同認可的約定—「契約」,並且約定內容的變化會被及時感知;其次,將系統之間的集成測試,轉換為由契約生成的單元測試,例如通過契約描述的內容,構建測試替身。這樣,依賴契約的測試效率優於集成測試,同時契約替代外部API成為信息變更的載體。

?

對於契約來講,行業內比較成熟的解決方案是基於YAML標記語言的Swagger Specification(OpenAPI Specification),或者是基於JSON格式的Pact Specification。

通常的做法是API的提供者使用「契約」的形式,將功能發布在公共平台,給調用方進行說明和參考,這裡我們可以暫時稱之為Provider-Driven-Contract。這種做法的潛在問題是,功能提供方的API返回內容是否都滿足所有API調用者的需求不得而知。所以,針對這個問題,依賴關係再一次反轉,契約測試就搖身一變成為了Consumer-Driven-Contract test(CDCT), 通過給API提供方提供契約的形式,來完成功能的實現。

難道CDCT成為了問題終結者嗎?請聽後面分解。

註: 契約測試其中一個的典型應用場景是內外部系統之間的測試,另一個典型的例子是前後端分離後的API測試,這裡不做過多展開。

契約測試的維度

1.測試覆蓋範圍對比(縱向)

單元測試:對軟體中的基本組成單位的測試,大多數是方法函數的測試,運行速度快。

契約測試:對服務之間的功能進行的測試,運行速度基本與單元測試相同。

E2E 測試:對系統前後端或者不同系統之間的集成測試,大多通過模擬UI操作的方式實現,運行速度三者之中最慢。

?

2.測試效率對比(橫向)

環境依賴:

  • 單元測試:程序集
  • 契約測試:程序集、依賴契約文件、虛擬路由服務
  • 端到端測試:程序集、真實路由服務、前端UI
  • 運行速度: 單元測試 > 契約測試 > 端到端測試

Pact官方給出的幾個場景:

適用場景:

  • 團隊能把控開發過程中的Consumer和Provider端
  • 適合Consumer驅動開發的場景
  • 對於每個獨立的Consumer端,Provider端都能管理好需求。

不適用的場景:

  • 公共API或者是OAuth授權服務
  • Provider端和Consumer端沒有良好的溝通渠道
  • 針對性能的測試
  • Provider端的功能性測試(Pact只測試內容和請求格式)
  • 對於不同輸入有相同的輸出,並未達到驗證的目的
  • 當前測試輸入需要依賴之前測試返回的結果

以上對比說明契約測試所要解決的問題是替代系統之間的集成測試,通過契約和單元測試的方式加速系統運行。同時也說明契約測試存在一些不適用的場景,要依據使用場景區別對待。契約測試沒有取代單元測試以及E2E測試。

契約測試與CD的整合

最開始,我們的pipeline是這樣的,單元測試是獨立的測試,當通過單元測試後運行集成測試。此時集成測試成為了系統瓶頸,而且一旦集成測試失敗,就必須被迅速修復,其他pipeline只能等待其修復,否則任何新的變更都會測試失敗。

一個解決辦法是將集成測試分散在每個pipeline上,每次集成測試運行的版本是當前的最新代碼和其他系統的上一次通過版本之間的測試。這樣解決了測試的獨立性以及不會阻礙其他pipeline測試的效果,然後將通過測試的不同系統的package按照版本保存。但是這樣一來,集成測試的缺點就更為明顯提現出來,第一是系統部署時間長,每次集成測試需要運行同樣的測試在不同pipeline上,增加了測試成本和反饋周期。

?? ?? 接下來,我們使用契約測試替代集成測試。這樣有幾點好處不僅解決了獨立測試的目的,同時解決了集成測試慢和部署時間長等問題。

為了保證契約測試的正確性,契約文件由Consumer端生成,然後Provider端來實現API,我們使用CDCT來改造我們的pipeline。

我們先假設B系統希望A系統提供新功能,如果按照圖中黃色步驟來提交的話,則會測試失敗,原因在於此時,契約文件是最新的B-A.consumer.1.1.pact與之對應A-B.provider.1.0.jar不是最新的,所以測試失敗。

按照圖中步驟2運行,當提交A的pipeline時,當前版本的A已經升級到1.1,而契約文件還是1.0版本,沒有break測試的情況下,最終將A-B.provider.1.1.jar提交到伺服器上。?

然後按照圖中步驟3運行,A-B.provider.1.1.jar和B-A.consumer.1.1.pact完美契合,最終又將B-A.consumer.1.1.pact提交到伺服器。所以,改成CDCT之後,雖然產生了一定的提交順序依賴,但是帶來的更多的好處是確保契約文件的產生是調用端提出,並且保證當前最新,確保系統的正確性。

喜歡思考的同學不難發現,CDCT存在自身的缺陷,一個簡單的例子是當B存在一個已有的契約約束A的一個功能,當B需要A更新其API時,是先提交B的契約測試,還是更改A的功能到最新版本?其實二者都不可行。

解決辦法萬變不離其宗,就是大家熟悉的不能再熟悉的重構心法,由王建總結的十六字箴言:?

我們分五步來完成API的更新:

  1. Provider端提交一個新的API來保證新功能,同時舊的API功能不變,提交並通過測試。
  2. 將Consumer端API的調用指向Provider端的新API,並更新契約文件以約束新功能。
  3. 將Provider端舊API同步更新為新API,提交並通過測試。
  4. 將Consumer端指回舊有API,其他保持不變。
  5. 將Provider端臨時過渡的新API刪除。

至此,我們解決了API更新時如何保證契約測試的提交順序,如果是刪除API,則直接刪除Consumer端的契約測試即可。

需要思考的問題:

1.如果並行測試的話,誰先提交成功的版本,另外一個測試是否要重新運行?

設想,當兩個並行pipeline A和B,同時運行時,A中跑的是A1.1和B1.0,B中跑的是B1.1和A1.0的測試,假如雙方均能通過各自的測試,但是新版本不兼容(A1.1和B1.1測試失敗),雙方都將各自的新版本保留,這樣就造成了存在相互不兼容的兩個版本。目前解決方案是,人為製造一個「瓶頸」,保證同時只有一個契約測試在運行,保存的只有一個版本。

2.契約測試可維護性如何?

構建契約測試類似於單元測試,並且在Pact的框架下十分方便維護。但是,測試框架本身還有一些問題,諸如,大小寫敏感,空值驗證,只有一份契約文件,契約測試分組等。

(以上是基於pact 1.0的實踐,pact2.0使用了正則表達式以及TypeMatching等機制解決了驗證「具體」值的問題,更多詳細內容請關注pact官方文檔)

結語

契約測試不是銀彈,它不是替代E2E測試的終結者,更不是單元測試的升級換代,它更偏向於服務和服務之間的API測試,通過解耦服務依賴關係和單元測試來加快測試的運行效率。

文/ThoughtWorks 崔彥松

推薦閱讀:

TAG:軟體測試 | IT行業 | 軟體 |