契約測試
◎ 契約測試概述
◎ 契約測試與TDD
◎ 契約測試與獨立交付
◎ 契約測試的相關技術與用法實戰(zhàn)
在微服務架構中最常見的事情就是遠程調用,如服務和服務之間的遠程調用,前端和后端之間的遠程調用,BFF和服務之間的遠程調用,等等。當一個服務的接口發(fā)生變化時,依賴它的消費者也需要進行相應的調試或修改,如果這個過程采用口口相傳或文件通知的辦法,就會很低效,而且容易遺漏。在大多數(shù)時候服務端并不能清楚地知道全部的消費者有哪些,哪個接口會影響哪個消費者。這時就需要一種自動的方法來幫助我們測試接口的可靠性,這就是契約測試。
契約測試概述
契約也就是合約,是雙方當事人意見一致并且要共同遵守的行為表示,服務的調用者和提供者就好比簽訂契約的甲方和乙方。契約測試就是驗證簽訂契約雙方的行為是否符合契約。
通常我們并不知道服務間的依賴關系是怎樣的,如每個接口的消費者是誰,相同的接口不同的消費者都需要哪些數(shù)據(jù),這些消費者正在消費哪個版本的接口等,要在一個項目中理清這些問題顯然有些困難,哪怕管理做得再好,也不可能面面俱到,而且文件記錄和實際情況往往會有差距。

如何能準確地檢測接口的變化所帶來的影響?是否管理所有服務端與消費者之間的關系?雖然這樣做看似可行且最直接,但是要管理所有接口的版本、調用關系等信息無疑是一個巨大的工程,而且也只能完成快速定位接口,并不能完全保證把影響降低,解決這些影響。一旦有遺漏,就意味著系統(tǒng)有問題。
契約測試的做法能解決上述問題,在微服務中,無論是服務與服務之間,還是服務與前端之間,抑或是服務與API Gateway之間,只要雙方有遠程調用的依賴關系,都可以定義一個關于雙方所依賴的接口契約,約定好接口的請求和返回信息,包括地址、參數(shù)、頭部、響應數(shù)據(jù)等,并且最好通過Git等版本管理工具將契約管理起來。

由服務提供者和調用者共同維護,雙方需要嚴格遵守這份契約,通常我們會將這份文件的信息解析出來,作為雙方單元測試的基準,然后消費者和服務者雙方都會測試自己的服務或請求是否遵守這份契約的規(guī)則,從而保證雙方依賴接口的正常使用,契約測試示意如圖4.1所示。

只要一方發(fā)生變化,就會導致測試的失敗,然后變化的一方就會去更改契約并且通知相關接口調用者。假設忘記通知其中一個調用者,這個調用者在使用最新的契約進行測試時,也會測試失敗,然后就會發(fā)現(xiàn)這個接口的變化,最后溝通并修復問題,這就是契約測試。
這樣做的好處是,當服務端接口變動后,只需修改對應的契約文件,就能讓契約的另一方測試失敗,準確地分析接口的影響范圍,并且如果契約測試是自動化的,整個過程成本極低,而且高效、準確,這樣我們就能通過自動化測試的手段,最大限度地避免人為的遺漏,保證服務提供者和調用者之間依賴的正確性。
而消費者如果想對接口進行調整,同樣可以修改契約文件,然后服務端的契約測試就會失敗,保證服務提供者對于接口的驗證。
這份契約不僅可以作為服務端和客戶端的邏輯驗證,還可以用來模擬一個后端的服務,接口的調用者就不用等到服務開發(fā)完成后才能調試程序,服務提供者和調用者雙方通常會在最開始定義好接口的契約,然后服務提供者依據(jù)契約去開發(fā)接口,服務調用者則可以使用契約模擬一個假的服務實例,通常稱這個假的服務實例為Mock Server。調用者會先用Mock Server來開發(fā)自己的程序,等真實的服務開發(fā)好后再進行集成測試,這種做法在前后端分離開發(fā)中尤為常見,如圖4.2所示。

契約測試與TDD
測試方式有很多種,如單元測試、集成測試、E2E測試、冒煙測試等,對于開發(fā)人員來講,接觸最多的是單元測試。契約測試也是單元測試的一種,說起單元測試就需要提到TDD,接下來了解一下契約測試在TDD中的實踐。
TDD的定義
TDD(Test-Driven Development,測試驅動開發(fā))是一種軟件開發(fā)過程中的應用方法,提倡在編寫代碼時先寫出測試,然后編碼實現(xiàn),編碼的目的就是讓測試通過,以達到一種由測試驅動開發(fā)的過程,并因此得名TDD。
這個方法最早是由XP(Extreme Programming,極限編程)提出來的,XP是一種軟件工程的方法學,也是敏捷軟件開發(fā)中最高效的幾種方法之一,它更強調可適應性而不是可預測性。XP認為軟件需求的不斷變化是軟件項目開發(fā)中不可避免的現(xiàn)象,應該欣然接受,與其在項目初始階段費盡心思地定義和控制需求,不如將精力放在建設軟件的適應能力上,所以我們需要不斷地重構,并且需要測試為重構保駕護航,TDD可以說是XP中十分重要的一環(huán),想深入了解的讀者可以查閱相關資料。
什么是TDD,應該如何做TDD?例如,如果現(xiàn)在要實現(xiàn)一兩個整數(shù)相加的功能,那么TDD開發(fā)步驟應如圖4.3所示。

首先寫一個測試,代碼如下。

這個測試是我們期望的一個基本結果,如期望有一個Calculator類,提供一個add方法,并且執(zhí)行add(1,1)的結果是2。顯然,這時編譯會報錯,因為還沒有編寫任何一行實現(xiàn)代碼,這時以最少量的代碼先讓代碼編譯通過。
為了保證編譯通過,編寫一個Calculator類,代碼如下。

然后運行測試,出現(xiàn)空指針異常,原因是變量calculator為空,再次增加代碼來解決空指針的問題,測試代碼改造如下。

再次運行測試,運行正常,但測試結果失敗,報錯如下。

顯然期望結果是2,但實際得到的結果是0,快速修改代碼來嘗試讓測試通過,修改add方法,代碼如下。

再次運行測試,測試成功,不過還需要編寫新的測試來讓測試失敗,添加測試如下。

再次運行測試,不出意外測試再次失敗,錯誤如下。

這時繼續(xù)重構我們的實現(xiàn),代碼如下。

再次運行測試,測試通過,我們還需要再次增加測試代碼來查看這次的實現(xiàn)邏輯是否有問題,代碼如下。

再次運行測試,測試依然通過。如果不放心,可以繼續(xù)增加一些其他條件的輸入?yún)?shù),盡量是一些邊界值或不同的情況組合,如果此時測試仍然能夠通過,基本上就算完成了該功能的開發(fā)。
這就是通過TDD的方式開發(fā)一個功能的過程,雖然這個場景比較簡單,但已經(jīng)演示了TDD的精髓所在。有時我們在所有測試通過后,可能還會加入一些重構代碼的環(huán)節(jié)。例如,在測試通過的過程中,會產(chǎn)生一些“壞味道”的代碼,帶來如代碼冗余、復雜度過高、信息鏈等問題,這時我們就需要進行重構,重構會出現(xiàn)新的問題,導致測試再次失敗。有測試保障代碼的邏輯,我們就可以進行放心大膽的重構,繼續(xù)TDD讓測試再次通過。綜上所述,從TDD的過程可以得出如圖4.4的流程。

從圖4.4可以看出,在TDD的過程中,實現(xiàn)功能之前需先編寫會失敗的測試,然后以最少的代價編寫具體的實現(xiàn)代碼讓測試通過,在測試通過后,再對代碼進行重構,測試可能失敗,或者再次嘗試編寫一些會失敗的測試,再繼續(xù)整個過程。
TDD的價值
國內對于TDD的理解同樣十分模糊,有很多人對TDD有一定的誤解,認為其只適合不擅長前期代碼設計的初級開發(fā)人員,或者認為TDD會增加開發(fā)人員的工作量,損害生產(chǎn)力,所以它只適合大公司,小團隊沒有時間去執(zhí)行TDD,當然也有完全相反的觀點,認為TDD只適合初創(chuàng)公司。盡管TDD擁有眾多忠實的支持者,但也不乏反對者,理由如下。
(1)使開發(fā)效率低下。
(2)不重視設計,總想著重構。
(3)過于保守,會因為怕破壞測試而不想重構。
(4)太過于注重細節(jié)而忽略整體設計。
(5)適合新手或者初創(chuàng)公司。
(6)大型企業(yè)才有時間去執(zhí)行。
……
從上述理由中發(fā)現(xiàn)大家的觀點都是不愿意TDD,但理由卻自相矛盾。那為什么需要TDD,TDD又有哪些好處?
TDD更像是一種設計方法。編寫單元測試的行為更像是一種設計行為,是一種圍繞需求核心價值可以落地實施的設計行為,能更好地幫助開發(fā)人員理解軟件的需求和驗收條件,更好地進行思考和設計。
TDD使單元測試更有價值。表面上看,從我們使用其開發(fā)的話,工作量好像變多了,但不采用TDD,單元測試可以省略嗎?一個缺少單元測試覆蓋的項目,無論是開發(fā)還是重構,都會有危險。很多時候,我們在事后為了提高測試覆蓋率而編寫的測試代碼并不能完全契合最初的開發(fā)意圖,甚至只會編寫一些HAppy Path的測試,從而導致項目的測試覆蓋率上去了,但測試大多數(shù)都是沒有價值的,而使用TDD,代碼就是為了使測試通過而編寫的,測試已不只是契合開發(fā)意圖的,此時的測試就是開發(fā)意圖,這樣的測試才更有價值。當我們擁有相對全面而完善的單元測試時,代碼就像是強壯的護衛(wèi),可以放心大膽地進行優(yōu)化、重構等工作而不需要人工進行反復回歸。

TDD幫助我們分解開發(fā)步驟,使開發(fā)更具有目的性,同時幫助我們理清開發(fā)思路,使開發(fā)時有條不紊、步驟分明。我們可以將一個簡單的加法運算拆分成可執(zhí)行的每個步驟。同樣,在面對復雜問題時,TDD仍然可以將問題拆分成具體的軟件需求,通過結果導向,考慮功能的輸入和輸出,來指導開發(fā)逐步實現(xiàn)需求。
TDD更加契合敏捷的思想,需要保證系統(tǒng)的適應力,正如在圖4.4中所展示的,重構在TDD中是重要的一環(huán),不斷讓測試失敗、通過,就是為了讓代碼能夠更加安全、靈活地重構。
綜上所述,通過整體分析可以發(fā)現(xiàn)使用TDD反而會提高開發(fā)效率,因為它更加重視設計,能夠減少人工的代碼回歸,幫助開發(fā)人員進行任務分解,更加契合敏捷思想。
TDD的種類
前面提到TDD比較注重細節(jié),會忽略整體規(guī)劃。其實,TDD分為ATDD(Acceptance Test Driven Development)和UTDD(Unit Test Driven Development)兩個概念。
ATDD即驗收測試驅動開發(fā),它的實踐一直在用,只是并未與理論對應。例如,在開始編寫業(yè)務代碼前,無論是敏捷還是瀑布,都會由產(chǎn)品經(jīng)理或業(yè)務分析師,抑或測試人員編寫驗收測試用例,然后開發(fā)會依據(jù)這次測試用例深入理解系統(tǒng)需求,這一過程甚至對代碼能起到一定的指導和驅動作用,這就是ATDD。
UTDD即單元測試驅動開發(fā),更多的由開發(fā)人員自己完成。例如,在4.2.1節(jié)的例子中,開發(fā)人員編寫單元測試用例,然后編寫實現(xiàn)代碼讓測試通過,再編寫測試試圖讓測試失敗,然后重構實現(xiàn),再次讓測試通過。
很明顯,UTDD比較關注細節(jié),在單元測試中,其更加關心代碼和技術實現(xiàn)本身,而ATDD更關注系統(tǒng)的整體業(yè)務需求和結果,所以說TDD只關注細節(jié)顯然太片面,如圖4.5所示。

契約測試也是TDD
我們來回想一下契約測試,如果說TDD是一種軟件方法,那么契約測試更像一種工程實踐,在前面的內容中了解到,在契約測試中需要定義一份契約文件,這份契約可以作為前端接口的Mock Server服務于前端開發(fā),也會作為后端接口的驗證條件,從而驅動后端服務接口的開發(fā)。
通常,我們會在開發(fā)之前就定義好契約,因為服務調用的一方常常依據(jù)契約生成對應的Mock Server,保證雙方都能夠并行開發(fā)。不難看出,一旦我們應用了契約測試,無形中就開始了TDD的第一步,契約就是我們最開始定義的失敗測試。為了讓契約測試通過,我們會進一步編寫實現(xiàn)代碼,無論怎樣重構,只要需求沒有變化,契約也不會發(fā)生變化,契約測試會保證接口的正確性。這樣看來,契約確實與TDD有異曲同工之處。
如果要進行單元測試,通常契約測試只會針對接口層的測試,若要開發(fā)一個Controller,則其契約測試步驟如圖4.6所示。

首先寫一個測試,代碼如下。

這個測試是我們期望的一個基本結果,如期望有一個Calculator類,提供一個add方法,并且執(zhí)行add(1,1)的結果是2。顯然,這時編譯會報錯,因為還沒有編寫任何一行實現(xiàn)代碼,這時以最少量的代碼先讓代碼編譯通過。
為了保證編譯通過,編寫一個Calculator類,代碼如下。

然后運行測試,出現(xiàn)空指針異常,原因是變量calculator為空,再次增加代碼來解決空指針的問題,測試代碼改造如下。

再次運行測試,運行正常,但測試結果失敗,報錯如下。

顯然期望結果是2,但實際得到的結果是0,快速修改代碼來嘗試讓測試通過,修改add方法,代碼如下。

再次運行測試,測試成功,不過還需要編寫新的測試來讓測試失敗,添加測試如下。

再次運行測試,不出意外測試再次失敗,錯誤如下。

這時繼續(xù)重構我們的實現(xiàn),代碼如下。

再次運行測試,測試通過,我們還需要再次增加測試代碼來查看這次的實現(xiàn)邏輯是否有問題,代碼如下。

再次運行測試,測試依然通過。如果不放心,可以繼續(xù)增加一些其他條件的輸入?yún)?shù),盡量是一些邊界值或不同的情況組合,如果此時測試仍然能夠通過,基本上就算完成了該功能的開發(fā)。
這就是通過TDD的方式開發(fā)一個功能的過程,雖然這個場景比較簡單,但已經(jīng)演示了TDD的精髓所在。有時我們在所有測試通過后,可能還會加入一些重構代碼的環(huán)節(jié)。例如,在測試通過的過程中,會產(chǎn)生一些“壞味道”的代碼,帶來如代碼冗余、復雜度過高、信息鏈等問題,這時我們就需要進行重構,重構會出現(xiàn)新的問題,導致測試再次失敗。有測試保障代碼的邏輯,我們就可以進行放心大膽的重構,繼續(xù)TDD讓測試再次通過。綜上所述,從TDD的過程可以得出如圖4.4的流程。

從圖4.4可以看出,在TDD的過程中,實現(xiàn)功能之前需先編寫會失敗的測試,然后以最少的代價編寫具體的實現(xiàn)代碼讓測試通過,在測試通過后,再對代碼進行重構,測試可能失敗,或者再次嘗試編寫一些會失敗的測試,再繼續(xù)整個過程。