聊一聊契約測試

什么是契約

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

《現藏于臺北故宮博物院》

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

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

發展歷程

接下來讓我們把時間回溯到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測試,通過解耦服務依賴關系和單元測試來加快測試的運行效率。


更多精彩洞見,請關注微信公眾號思特沃克

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,327評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,996評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,316評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,406評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,128評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,524評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,576評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,759評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,310評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,065評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,249評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,821評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,479評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,909評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,140評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,984評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,228評論 2 375

推薦閱讀更多精彩內容