在之前寫的《契約測(cè)試之Pact By Example》中,我曾提到會(huì)再寫一篇文章,來聊聊如何正確地認(rèn)識(shí)和理解契約測(cè)試(好吧,至少是我認(rèn)為的"正確地")。但在隨后的一年多時(shí)間里,對(duì)契約測(cè)試的討論漸漸淡出了我的視野。我的理解是,隨著微服務(wù)的大行其道,契約測(cè)試作為帶刀護(hù)衛(wèi),已經(jīng)深入人心了,所以沒必要再去炒這碗冷飯,就像現(xiàn)在已經(jīng)沒有誰會(huì)再來碼字吹Selenium一樣(...請(qǐng)相信,我一定不是因?yàn)閼胁胚@么說的o(* ̄3 ̄)o)。
然而,在最近參加的一次面向Dev的后端分享的討論中,我意外的發(fā)現(xiàn),契約測(cè)試作為構(gòu)建微服務(wù)重要的一環(huán)工程實(shí)踐,雖然確實(shí)已經(jīng)被團(tuán)隊(duì)原生接受,但對(duì)于契約測(cè)試的理解,還存在一些認(rèn)識(shí)上的盲點(diǎn),特別是當(dāng)契約測(cè)試與集成測(cè)試、接口測(cè)試一起討論的時(shí)候,理解的偏差往往會(huì)被放大不少。所以,我想必要的碼點(diǎn)字,分享一下我對(duì)契約測(cè)試的理解,還是有益的。
"契約測(cè)試,是建立在服務(wù)的消費(fèi)者和生產(chǎn)者之間的......"(此處省略廢話N多字),如果您要繼續(xù)看下去,請(qǐng)注意:
- 以下的內(nèi)容不會(huì)涉及基本的契約測(cè)試概念,比如消費(fèi)者、生產(chǎn)者、契約、消費(fèi)者驅(qū)動(dòng)等等,如果您對(duì)這些基本概念還不是很清楚,建議您可以花點(diǎn)兒時(shí)間先google一下,當(dāng)然,Pact的官方文檔可以是一個(gè)很好的開始;
- 以下的內(nèi)容不會(huì)涉及具體的契約測(cè)試編寫和執(zhí)行步驟,相關(guān)的內(nèi)容,您可以參看我之前的文章《契約測(cè)試之Pact By Example》;
- 如果您之前在任何地方、通過任何方式,看到過一些我對(duì)契約測(cè)試的觀點(diǎn)的分享,并且覺得我就是在胡說八道,那您也不用看下去了,因?yàn)楹竺娑际呛f十六道,而已;
關(guān)于測(cè)試的表述
在聊契約測(cè)試之前,讓我們先來說一些平時(shí)看似毫不起眼的小話題---"測(cè)試的表述"。
"我們可以在E2E測(cè)試中覆蓋這個(gè)場(chǎng)景,而不是單元測(cè)試..."
或者
"你們的E2E測(cè)試是怎么做的?..."
這里的E2E測(cè)試可能經(jīng)常出現(xiàn)在我們的日常交流中,那你知道它的準(zhǔn)確含義嗎?答案是沒有含義!它基本等價(jià)于你們一伙人去食堂吃飯(...笑啥,俺就是食堂黨,咋的!),A:"今天吃啥?",B:"新鮮的"。新鮮的啥?炒飯?面條?餃子?套餐?......
E2E,End To End,端到端,字面意思簡(jiǎn)單明了,但它只是一個(gè)副詞(組),而不是一種測(cè)試類型。所以,我們真正想表述的,可能是E2E API Test。那么"E2E API Test"就完整的表述了一項(xiàng)測(cè)試活動(dòng)了嗎?不是的!E2E表示的是測(cè)試方式,API表示的是被測(cè)對(duì)象,但這里,我們還缺少被測(cè)對(duì)象的被測(cè)屬性,比如,F(xiàn)unction、Performance, Security等等,所以,一個(gè)比較完整的表述,往往可以是這樣的:

當(dāng)然,平常的交流中,一般不會(huì)這么文縐縐地去摳字眼,因?yàn)槲覀儽舜硕记宄懻搯栴}的上下文,這點(diǎn)很重要。特別是針對(duì)E2E測(cè)試這樣的表述。比如,我們有一個(gè)前后端分離、后端是微服務(wù)集群的系統(tǒng)應(yīng)用,同樣的E2E測(cè)試可能就代表著完全不同的測(cè)試活動(dòng):

如果從更多的維度來思考,比如套上測(cè)試四象限的模式,那么對(duì)于測(cè)試活動(dòng)的表述,還會(huì)有更多考量。但今天的主題是關(guān)于契約測(cè)試的,所以就不過多的展開了。為什么要在討論契約測(cè)試之前來廢話"測(cè)試表述"呢?因?yàn)槠跫s測(cè)試其實(shí)是多種測(cè)試方式的和思維的復(fù)合產(chǎn)物,比如,契約測(cè)試是E2E的測(cè)試嗎?還是說是基于Mock的?契約測(cè)試是服務(wù)的接口測(cè)試還是集成測(cè)試?等等。所以,如果對(duì)這些基本的測(cè)試概念不是很清楚的,很容易迷失在契約測(cè)試的理念中。
為什么要做契約測(cè)試?
為什么要做契約測(cè)試?"因?yàn)槲覀兪俏⒎?wù)"?(╬ ̄皿 ̄)=○
很多回答這個(gè)問題的答案,都關(guān)注在契約測(cè)試的目的上。那么,什么是契約測(cè)試的目的呢?簡(jiǎn)單來說,契約測(cè)試就是為了發(fā)現(xiàn)契約破壞(Contract Breaking)而進(jìn)行的測(cè)試活動(dòng)。如果你使用過Pact或者Spring Cloud Contract,你會(huì)發(fā)現(xiàn),契約測(cè)試本身也是通過調(diào)用Provider的API接口來獲取Response,再與契約文件中期望的結(jié)果做對(duì)比,從而驗(yàn)證契約是否正確。形式上,這和我們的API接口測(cè)試,或者針對(duì)功能的集成測(cè)試(以下簡(jiǎn)稱集成測(cè)試,因?yàn)槲覀冞@里不討論API的安全、性能等問題)是非常類似的。換句話說,我們通過API的接口測(cè)試或者集成測(cè)試,也能達(dá)到檢查契約的目的,那為什么還要做契約測(cè)試呢?這種思考邏輯是完全正確的,也是為什么很多初學(xué)者都認(rèn)為契約測(cè)試沒有必要的原因。
那再問,為什么我們還要做契約測(cè)試呢?真正能夠回答這個(gè)問題的,不是契約測(cè)試的目的,而是契約測(cè)試可以帶來的價(jià)值!
契約測(cè)試的價(jià)值
那什么是契約測(cè)試的價(jià)值呢?要說清楚契約測(cè)試的價(jià)值,就需要準(zhǔn)確認(rèn)識(shí)契約測(cè)試的精髓--"消費(fèi)者驅(qū)動(dòng)"。
消費(fèi)者驅(qū)動(dòng)的字面含義,大家都清楚,但往往容易忽略的是被驅(qū)動(dòng)的對(duì)象。在討論契約測(cè)試的范疇里,"消費(fèi)者驅(qū)動(dòng)"述及的對(duì)象是契約,而不是契約測(cè)試。
當(dāng)某個(gè)provider正常上線后,某個(gè)consumer需要消費(fèi)這個(gè)provider的服務(wù),那么應(yīng)該由consumer來提出期望建立它們之間的契約測(cè)試。因?yàn)椋?code>契約測(cè)試,形式上,雖然測(cè)試的是provider,但,價(jià)值上,保證的卻是consumer的業(yè)務(wù)。如果consumer對(duì)自己都不上心,你還期望provider來時(shí)刻關(guān)注你的死活嗎?別笑,在跨團(tuán)隊(duì)的微服務(wù)體系下,這些都是真切的痛點(diǎn)。
理清了消費(fèi)者驅(qū)動(dòng),就讓我們來看看契約測(cè)試真正的價(jià)值吧。一個(gè)經(jīng)典的案例:

在上圖一個(gè)簡(jiǎn)單的消費(fèi)關(guān)系中,provider為consumer A,B,C提供服務(wù)。provider自己提供的schema包含name,age和gender三個(gè)簡(jiǎn)單的字段。請(qǐng)注意,這份包含name,age和gender的JSON,其本身,只是一個(gè)schema,并不是任何契約。契約一定是成對(duì)存在的,沒有確切consumer的交互定義,只是schema,不是契約。一個(gè)列子,中介打印了一份合同,上面寫好了房屋租賃的全部信息,但在房東和租客都簽字之前,這份"合同"并不具有任何效力,所以它根本就不是一份有意義的合同,法律上,它叫"要約"。(...感謝我大學(xué)的法律老師,我居然還記得這個(gè)詞兒)
現(xiàn)在,這里有三份契約(對(duì)應(yīng)的,就應(yīng)該有三份契約測(cè)試),consumer A消費(fèi)provider的age和gender,consumer B消費(fèi)name、age和gender,consumer C消費(fèi)name和gender。就目前provider提供的schema來說,沒有任何問題,大家相安無事。
某日,因?yàn)闃I(yè)務(wù)需求,consumer C期望provider提供更加詳細(xì)的name信息,包括firstName和lastName。這個(gè)需求對(duì)provider并不困難,所以,provider打算對(duì)schema做類似下面的修改。

這樣的修改,很明顯,對(duì)consumer C是需要的,對(duì)consumer A無所謂,但對(duì)consumer B卻是不可接受的,屬于典型的契約破壞。此時(shí),provider和consumer B之間的契約測(cè)試就會(huì)掛掉,從而對(duì)provider提出預(yù)警(至于,剩下的,怎么協(xié)調(diào)和consumer B的兼容問題,就不是契約測(cè)試關(guān)注的問題,那需要的是團(tuán)隊(duì)間的communication)。
上面這個(gè)示例中的一些細(xì)節(jié),可以幫助我們發(fā)掘契約測(cè)試的價(jià)值點(diǎn):
"consumer A沒有使用name,consumer C沒有使用age",
基于消費(fèi)者驅(qū)動(dòng)的契約測(cè)試,契約的內(nèi)容由consumer提供,其內(nèi)容體現(xiàn)的是各個(gè)consumer對(duì)provider提供的schema的消費(fèi)需求。這里的需求,不光包含consumer"需要什么",還包含consumer"不需要什么"。這是非常有意義的,因?yàn)楫?dāng)你發(fā)現(xiàn)provider提供的schema的某些部分不被任何consumer消費(fèi)時(shí),就代表provider可以對(duì)schema的這些內(nèi)容做任意的修改,完全不必?fù)?dān)心會(huì)影響到任何consumer。這是契約測(cè)試非常重要的價(jià)值點(diǎn)。
"單個(gè)provider多個(gè)consumer",
要最大化的體現(xiàn)契約測(cè)試異于集成測(cè)試的價(jià)值,一定是在"單個(gè)provider對(duì)應(yīng)多個(gè)consumer"的架構(gòu)下來說的。因?yàn)?,在只有一個(gè)provider和一個(gè)consumer的架構(gòu)下,只存在一份契約,對(duì)該契約內(nèi)容的任何修改,對(duì)這對(duì)provider和consumer來說,都是顯而易見的,那么就不會(huì)出現(xiàn)契約破壞的情況。說人話,就是,如果是consumer提出要修改契約,consumer一定知道改怎么消費(fèi)新的契約內(nèi)容;如果是provider提出修改契約,對(duì)于唯一的一個(gè)consumer,provider能很方便的告知其將要對(duì)契約的修改。并且,在這種情況下,集成測(cè)試往往就已經(jīng)完整的達(dá)到了契約測(cè)試的目的。
而在單個(gè)provider對(duì)應(yīng)多個(gè)consumer的架構(gòu)下,情況就大不一樣了。provider和consumer C之間的契約修改,對(duì)consumer A無感,對(duì)consumer B卻是契約破壞,對(duì)此,集成測(cè)試是無能為力的。仔細(xì)來看,這里有4個(gè)service,就會(huì)有4個(gè)集成測(cè)試。但每個(gè)集成測(cè)試都只會(huì)關(guān)注自己的業(yè)務(wù)正確性,具體來說:
consumer A,因?yàn)椴皇苡绊?,所以A的集成測(cè)試沒有任何變化;
consumer C,因?yàn)槭瞧跫s修改的提出者,所以它會(huì)在provider提供新的schema后修改自己的集成測(cè)試,沒有問題;
provider,如果接受了consumer C的需求,大搖大擺地修改了schema,它也會(huì)相應(yīng)的修改自己的集成測(cè)試,因?yàn)閷?duì)provider來說,這個(gè)變更是正常的業(yè)務(wù)需求,也沒有問題;
consumer B,最倒霉,啥都沒干就掛了,當(dāng)然,它的集成測(cè)試會(huì)捕捉到這個(gè)failure,但那都是在provider的契約破壞生效之后的事情了,能做的也只有亡羊補(bǔ)牢。
可見,雖然4個(gè)集成測(cè)試都各司其職,但都不能對(duì)這個(gè)契約破壞的問題做到防患于未然!只有契約測(cè)試,才是這個(gè)問題的最佳答案!這就是契約測(cè)試最大的價(jià)值,它只會(huì)在"單provider多consumer"的環(huán)境下(這是微服務(wù)的常見場(chǎng)景,但不是必然場(chǎng)景),才能發(fā)揮出來。
"很顯然,對(duì)consumer A無害,但對(duì)consumer B卻是契約破壞",
"很顯然",僅僅是對(duì)于我們這個(gè)簡(jiǎn)單得不能再簡(jiǎn)單的示例而言,真正的業(yè)務(wù)場(chǎng)景下,特別是一些復(fù)雜的微服務(wù)集群,又或者是一些時(shí)間跨度很長(zhǎng)的系統(tǒng),對(duì)于某個(gè)provider,到底有多少個(gè)consumer?而provider的每一處修改,又到底會(huì)對(duì)哪些consumer的契約造成怎樣的影響?這些往往都是很難確定的問題。我最近所在的一個(gè)集團(tuán)項(xiàng)目上,一個(gè)搜索地址的基礎(chǔ)服務(wù)provider,有十個(gè)左右的consumer,其中有八個(gè)consumer沒有契約測(cè)試,就不清楚它們對(duì)provider的API具體是如何消費(fèi)的,所以每次provider要更新,就得八方去通知這些consumer的團(tuán)隊(duì)來做回歸測(cè)試。有時(shí),一點(diǎn)小小的修改,回歸測(cè)試一分鐘就可以搞定,但人肉聯(lián)系各個(gè)團(tuán)隊(duì)卻會(huì)花上好幾天......
如果每個(gè)consumer都能和provider建立契約測(cè)試(這里我們暫且不考慮負(fù)載和去重的問題),通過類似Pact Broker這樣的實(shí)踐,我們就能很好的解決這些效率問題。

OK,理解透契約測(cè)試的這些價(jià)值后,對(duì)于"要不要做契約測(cè)試?"、"誰來做契約測(cè)試?"這些問題,相信你就不再疑惑了。想再次強(qiáng)調(diào)一下的是,契約測(cè)試很多情況下基于微服務(wù)而生,但并不代表每個(gè)微服務(wù)都一定需要契約測(cè)試。相對(duì)的,一些傳統(tǒng)的單體服務(wù),它的架構(gòu)設(shè)計(jì)和部署實(shí)施,完全和微服務(wù)的理念相反,但它提供的服務(wù)卻被眾多的下游消費(fèi)者使用,那么這樣的服務(wù),也有很強(qiáng)的契約測(cè)試需求。所以,千萬不要把契約測(cè)試和微服務(wù)做"死綁定",一定要基于服務(wù)的業(yè)務(wù)來考慮策略。
契約測(cè)試和接口測(cè)試、集成測(cè)試的區(qū)別
"契約測(cè)試和接口測(cè)試、集成測(cè)試的區(qū)別",從2015年我第一次在BQConf講契約測(cè)試,到寫這篇文章之前,最近一次和別人討論契約測(cè)試,這都是一個(gè)一直被提起的問題。在上面的內(nèi)容中,其實(shí)已經(jīng)或多或少的提到了相關(guān)的內(nèi)容。由于具體的測(cè)試方式,都是"調(diào)用API驗(yàn)證Response",契約測(cè)試、接口測(cè)試、集成測(cè)試經(jīng)常被放在一起來進(jìn)行比較,甚至質(zhì)疑彼此。
先讓我們來看看接口測(cè)試和集成測(cè)試。說實(shí)話,對(duì)于測(cè)試?yán)碚摵粚?shí)的QA來說,這里應(yīng)該沒有任何問題的,因?yàn)榻涌跍y(cè)試和集成測(cè)試,它們壓根兒就是從完全不同的維度來描述測(cè)試活動(dòng)的。
前面說過,如果要完整的描述一個(gè)測(cè)試活動(dòng),至少需要考慮三個(gè)內(nèi)容:測(cè)試方式、被測(cè)對(duì)象、被測(cè)屬性。然而,"接口測(cè)試"和"集成測(cè)試",顯然,都是我們根據(jù)上下文使用的簡(jiǎn)稱,更準(zhǔn)確的:
| 測(cè)試方式 | 被測(cè)對(duì)象 | 被測(cè)屬性 | |
|---|---|---|---|
| 接口測(cè)試 | 調(diào)用API接口 | 只能是API | ... |
| 集成測(cè)試 | ... | ... | 肯定是被測(cè)對(duì)象在于外部依賴集成時(shí)的行為表現(xiàn) |
接口測(cè)試
- 被測(cè)屬性 --- 不定,可以是被測(cè)對(duì)象的性能或安全行為,但根據(jù)上下文,默認(rèn)是功能行為;
集成測(cè)試
- 測(cè)試方式 --- 不定,可以直接進(jìn)行E2E的測(cè)試,也可以進(jìn)行基于Mock的測(cè)試;
- 被測(cè)對(duì)象 --- 不定,可以是UI,也可以是API,但根據(jù)上下文,默認(rèn)是API;
所以,基于不同的維度,我們有"接口測(cè)試"和"集成測(cè)試"的表述,但,當(dāng)放在和契約測(cè)試來討論的時(shí)候,它們描述的可能是同樣的測(cè)試活動(dòng)。即,通過調(diào)用API接口,來測(cè)試API的功能行為。
這里,想強(qiáng)調(diào)一下集成測(cè)試中的"集成"。對(duì)于傳統(tǒng)的瀑布開發(fā)模式,對(duì)應(yīng)的測(cè)試流程按照測(cè)試級(jí)別(Test Level)劃分,一般是:?jiǎn)卧獪y(cè)試 -> 集成測(cè)試 -> 系統(tǒng)測(cè)試 -> 驗(yàn)收測(cè)試,這是"集成測(cè)試"早期的由來。
那會(huì)兒的應(yīng)用,往往是龐大的單體服務(wù),服務(wù)內(nèi)部有分工明細(xì)、邊界分明的"模塊"。這些模塊被并行開發(fā),就緒后就會(huì)進(jìn)行彼此集成。集成的對(duì)象,一般可以簡(jiǎn)單分為:邏輯模塊、數(shù)據(jù)庫(kù)模塊、外部服務(wù)模塊。比如,在上古時(shí)代,對(duì)數(shù)據(jù)庫(kù)的操作是比較繁瑣的,開發(fā)人員往往需要自己組裝SQL語(yǔ)句,然后封裝成模塊來供上層調(diào)用。單元測(cè)試可以保證這些模塊自己的邏輯正確,但像"模塊中的各個(gè)函數(shù)接受的參數(shù)個(gè)數(shù)和參數(shù)類型是否和模塊使用者的需求相匹配"這樣的問題,就需要集成測(cè)試來確保(集成不等于集成測(cè)試,內(nèi)容所限,我就不過多說明了)。這些測(cè)試都是發(fā)生在單體服務(wù)內(nèi)部的,類似于現(xiàn)在的組件測(cè)試。
如今,微服務(wù)的設(shè)計(jì),將不同業(yè)務(wù)的"模塊"拆分成了不同的服務(wù),各個(gè)服務(wù)都是高內(nèi)聚的。以Spring為例,Controller -> Service -> Repository,內(nèi)部垂直劃分,簡(jiǎn)單明了。像上面提到的手寫SQL這樣的數(shù)據(jù)持久化工作,已經(jīng)基本不存在了,取而代之的是像spring-boot-starter-data-jpa或spring-boot-starter-data-mongodb這樣功能強(qiáng)大、方便易用的公共組件,最重要的,這樣的公共組件,一般都有很高的官方質(zhì)量保證的。所以,結(jié)論就是,在上古時(shí)代的那種傳統(tǒng)的集成測(cè)試,在微服務(wù)的體系下,已經(jīng)基本不需要了。
而對(duì)于單個(gè)微服務(wù)的質(zhì)量保障,特別是當(dāng)這個(gè)微服務(wù)有外部集成的時(shí)候,比如數(shù)據(jù)庫(kù)或者外部服務(wù),我們?nèi)匀恍枰M(jìn)行檢查外部集成的測(cè)試。再結(jié)合微服務(wù)業(yè)務(wù)的單一性,我們可以很自然的將這種"檢查外部集成的測(cè)試"合并到API的接口功能測(cè)試中。說人話就是,對(duì)于微服務(wù),只進(jìn)行API的接口功能測(cè)試,既涵蓋對(duì)被測(cè)服務(wù)領(lǐng)域邏輯的檢查,又覆蓋其對(duì)外部集成的檢查。
當(dāng)然,這里已經(jīng)討論到了微服務(wù)測(cè)試策略了,我就不再過多展開了。話收回來,如果要和契約測(cè)試進(jìn)行區(qū)別比較的話,我們只用考慮功能性的API接口測(cè)試就可以了。
理清了接口測(cè)試和集成測(cè)試的內(nèi)部姻緣(下面我統(tǒng)稱功能測(cè)試),我們就最后來說說它們和契約測(cè)試的區(qū)別吧~
其實(shí),上面那個(gè)示例,已經(jīng)很好的展現(xiàn)了它們的區(qū)別,我就不過多解釋了,簡(jiǎn)單來說:
功能測(cè)試關(guān)注的是provider的實(shí)現(xiàn)正確體現(xiàn)其設(shè)計(jì),契約測(cè)試關(guān)注的是provider的實(shí)現(xiàn)(當(dāng)然,肯定也包括設(shè)計(jì))滿足每一個(gè)consumer的需求。注意,功能測(cè)試只關(guān)注provider自身,契約測(cè)試關(guān)注每一個(gè)consumer;
功能測(cè)試的測(cè)試案例,由provider的團(tuán)隊(duì)提供,契約測(cè)試的測(cè)試案例,基于消費(fèi)者驅(qū)動(dòng),由各個(gè)consumer團(tuán)隊(duì)提供;
一個(gè)provider只會(huì)有一個(gè)功能測(cè)試(誰要糾結(jié)"一個(gè)功能測(cè)試"是幾個(gè)testcase,就把TA拖出去槍斃三分鐘),但契約測(cè)試,理論上,可以無限,有多少consumer就可以有多少個(gè)契約測(cè)試;
同樣的一個(gè)testcase,在功能測(cè)試?yán)锩娉霈F(xiàn)一次,在契約測(cè)試?yán)锩娉霈F(xiàn)N次,它們的含義是完全不同的。什么含義,自己琢磨琢磨;
一個(gè)testcase,出現(xiàn)在功能測(cè)試?yán)锩妫瑓s沒有出現(xiàn)在契約測(cè)試?yán)锩?,是非常有意義的。啥意義,再自己琢磨琢磨;
功能測(cè)試可以自?shī)首詷?,契約測(cè)試必須組"對(duì)"上分;
契約測(cè)試可以替代集成測(cè)試嗎?
"契約測(cè)試替代集成測(cè)試",說實(shí)話,第一次聽見這個(gè)說法的時(shí)候,我是非常驚訝的,這得多大的腦洞才能給出這樣的命題呀!
提示一下,就題論題,這里的"集成測(cè)試",并不全等與上面提到的"功能測(cè)試",僅僅是一般論的集成測(cè)試。
先來揣測(cè)一下,為什么會(huì)有這樣的問題吧。我們知道,在Pact(JVM)的實(shí)施過程中,第一步是在consumer端生成契約文件。這期間,Pact會(huì)根據(jù)自定義的契約,在consumer端啟動(dòng)一個(gè)mock server(如果你有看源碼,就知道它只是一個(gè)普通的HttpServer實(shí)例),consumer向這個(gè)mock server發(fā)送request獲取response,整個(gè)過程被記錄成JSON的契約文件。
這個(gè)流程的最后一步,一直有一個(gè)大家樂于爭(zhēng)論的話題:"要不要對(duì)response的內(nèi)容做斷言檢查?"。這是一個(gè)很開放的問題,沒有標(biāo)準(zhǔn)的答案。但我想強(qiáng)調(diào)的是,不加斷言,這一切只是一個(gè)"流程"或者說"步驟",加上斷言,它就是測(cè)試。是的,對(duì)consumer來說,它就是consumer的一種集成測(cè)試(啥?"用的是Mock Server,都沒有集成真正的provider,為什么叫集成測(cè)試?" 如果你有這個(gè)問題,可以再仔細(xì)想想集成測(cè)試的真正含義......)。
以上是解題背景?,F(xiàn)在,讓我們?cè)賮韺徱幌骂}吧,"契約測(cè)試可以替代集成測(cè)試嗎?",這里,其實(shí)隱藏了很大的一個(gè)意識(shí)盲點(diǎn)。契約測(cè)試,描述的測(cè)試活動(dòng),一定是架設(shè)在一對(duì)consumer和provider之間的。那么題目里的集成測(cè)試呢?你是想替換consumer端的集成測(cè)試?還是想替換provider端的集成測(cè)試?還是說其實(shí)你也不清楚到我想替換哪一端的集成測(cè)試......"不!我想說的不是兩個(gè)服務(wù)之間的集成的那種測(cè)試,而是整個(gè)系統(tǒng),包括全部上下游服務(wù),集成在一起的集成測(cè)試"......誒,好吧,那叫系統(tǒng)(E2E)測(cè)試......
還是讓我們回到一般論的集成測(cè)試上來吧(不然,要說的實(shí)在太多了 T_T),無論是consumer端還是provider端,集成測(cè)試的關(guān)注點(diǎn),是consumer是否可以正確的消費(fèi)provider的API,這里的"消費(fèi)"包括調(diào)用接口和解析數(shù)據(jù)。它的被測(cè)對(duì)象,注意,一定是consumer,或者說,是一個(gè)服務(wù)作為consumer的角色(因?yàn)?,某個(gè)服務(wù)經(jīng)常既是consumer,又是provider)。而契約測(cè)試的被測(cè)對(duì)象,一定是provider。好了,這就是問題的核心,其它的細(xì)節(jié),我想就不必再贅述了吧。
關(guān)于Pact和Spring Cloud Contract
"用Pact還是Spring Cloud Contract?",這是另一個(gè)經(jīng)常被討論的話題。它背后折射的卻是另一個(gè)非常重要的概念博弈:契約測(cè)試 vs 基于契約的測(cè)試(契約驅(qū)動(dòng)的測(cè)試)。
Pact的理念是消費(fèi)者驅(qū)動(dòng)的契約測(cè)試。什么是契約測(cè)試呢?目前,我沒有找到任何"權(quán)威"的定義。其實(shí),面向工程實(shí)踐的理念,也許根本就沒有權(quán)威,有的只是最適用于自身的實(shí)踐總結(jié)。即便如此,我還是希望以個(gè)人的視角,提供一些解讀:
如果你google搜索contract test,你得到的第一個(gè)答案肯定是Martin Fowler在2011年的這篇文章,但遺憾的是,老馬這里討論的契約測(cè)試,是解決在集成測(cè)試中,如何保證測(cè)試替身有效性的問題的,它和我們今天討論的契約測(cè)試并不是一回事。但是,如果拋開契約測(cè)試的內(nèi)容,而單論"契約測(cè)試"的定義的話,老馬的文章其實(shí)表述了一個(gè)很有價(jià)值的點(diǎn),那就是"契約是需要測(cè)試的",這是非常有意義的。
Pact的官方文檔,是另一個(gè)可以幫助我們理解契約測(cè)試的地方。它對(duì)契約測(cè)試給出了這樣的定義:
Contract testing is a way to ensure that services (such as an API provider and a client) can communicate with each other.,這里面需要關(guān)注的重點(diǎn)是communicate,它給出了Pact對(duì)契約測(cè)試范疇(scope)的定義。對(duì)于任何以"XXX測(cè)試"命名的測(cè)試活動(dòng),我們都遵循同樣的一個(gè)理解的公理:"XXX"一定是被測(cè)對(duì)象或被測(cè)屬性。比如,UI測(cè)試,測(cè)試對(duì)象一定是UI;安全測(cè)試,測(cè)試的一定是被測(cè)對(duì)象的安全表現(xiàn);兼容性測(cè)試,關(guān)注的一定是被測(cè)對(duì)象在兼容性方面的問題,等等。同樣的,"契約測(cè)試",被測(cè)對(duì)象一定是服務(wù)之間的契約。
好了,有了這三點(diǎn)重要的理論基礎(chǔ),就讓我們來具體看看Pact和Spring Cloud Contract(以下簡(jiǎn)稱SCC)的區(qū)別吧。

在上面的圖中,給出了Pact和SCC具體的使用方式(邏輯路徑)。當(dāng)然,如果你有一些基本的Pact或SCC的使用經(jīng)驗(yàn),就再好不過了。
Pact,在consumer端生成契約文件,發(fā)布到Pact Broker,而后,provider從Pact Broker獲取契約文件,觸發(fā)provider端執(zhí)行契約測(cè)試。
SCC,實(shí)際生成契約文件的工作是發(fā)生在provider端的,基于這份契約文件,在provider端,生成了Java的測(cè)試案例,這些測(cè)試案例用于provider的功能測(cè)試;而在Consumer端,使用同一份契約文件作為Stub,生成了基于WireMock的mock service,consumer可以使用該mock service來做集成測(cè)試。
可見,Pact作為消費(fèi)者驅(qū)動(dòng)契約測(cè)試的倡導(dǎo)者,真正地實(shí)踐了消費(fèi)者驅(qū)動(dòng)的契約測(cè)試。相對(duì)的,SCC,既沒有實(shí)際的將契約作為被測(cè)對(duì)象來進(jìn)行測(cè)試,更沒有確實(shí)地實(shí)現(xiàn)"消費(fèi)者驅(qū)動(dòng)"。SCC的做法,實(shí)際上是基于同一份契約,分別驅(qū)動(dòng)了consumer端的集成測(cè)試和provider端的功能測(cè)試。所以,Pact和SCC的區(qū)別,就在于,前者做的是"契約測(cè)試",后者做的是"基于契約的測(cè)試(契約驅(qū)動(dòng)的測(cè)試)"。
如果有同學(xué)閱讀過SCC的文檔,一定會(huì)質(zhì)疑,SCC明文寫著"Spring Cloud Contract Verifier enables Consumer Driven Contract (CDC) development of JVM-based applications",那為什么說它沒有確實(shí)地實(shí)現(xiàn)"消費(fèi)者驅(qū)動(dòng)"呢?因?yàn)樵赟CC的設(shè)計(jì)中,原始契約文件是在provider端生成的。為了實(shí)現(xiàn)CDC,consumer需要在其本地克隆provider的代碼倉(cāng)庫(kù),"借"provider來生成原始的契約文件。顯然,在現(xiàn)實(shí)的項(xiàng)目中,consumer團(tuán)隊(duì)不可能隨心所欲的獲取到provider代碼倉(cāng)庫(kù)訪問權(quán)限,所以有了后來的,基于Share Repo的解決方案,來實(shí)現(xiàn)契約的共享(編輯和使用)。所以說,從最初的設(shè)計(jì)思想來看,SCC并沒有像Pact那樣,"實(shí)實(shí)在在"地實(shí)踐了消費(fèi)者驅(qū)動(dòng)的契約測(cè)試。
那么,到底是選擇Pact(契約測(cè)試)還是SCC(基于契約的測(cè)試)呢?答案是"按需取舍"。
比較Pact和SCC的目的,并不是區(qū)別彼此的好壞長(zhǎng)短,而是闡述它們各自不同的測(cè)試?yán)砟睢act的價(jià)值點(diǎn),前面已經(jīng)說過了,SCC,雖然做的并不是真正的契約測(cè)試,但它通過共享(同一份)契約的方式,實(shí)現(xiàn)了微服務(wù)測(cè)試中,consumer和provider之間E2E集成測(cè)試的解耦,這在實(shí)際項(xiàng)目中,也是有重要的現(xiàn)實(shí)意義的。感興趣的同學(xué)可以自己下來多研究研究,我就不在這里擴(kuò)展了。
一些問題
至此,在我看來,契約測(cè)試相關(guān)的認(rèn)識(shí)難點(diǎn),就已經(jīng)基本解讀到了。但在結(jié)束全文之前,有兩個(gè)問題,我還想再闡述一下:
consumer端的集成測(cè)試需要做到什么程度?
對(duì)于Pact,前面提到,在consumer端生成契約文件的時(shí)候,加上斷言語(yǔ)句后,就"構(gòu)成"了consumer端的集成測(cè)試。這個(gè)集成測(cè)試,從Pact的角度來說,是可選的,它的目的是保證consumer端生成的契約文件本身是正確的。但從consumer的角度來說,要不要進(jìn)行這一層級(jí)的集成測(cè)試,取決于consumer團(tuán)隊(duì)自己的測(cè)試策略。我想說的是,如果要進(jìn)行這一層級(jí)的集成測(cè)試,請(qǐng)一定合理把握你的測(cè)試粒度和測(cè)試范疇。
測(cè)試粒度,由于這里的集成測(cè)試是和契約測(cè)試強(qiáng)綁定的,如果為了增加集成測(cè)試的覆蓋率而設(shè)定過小的測(cè)試粒度,會(huì)大大增加契約測(cè)試的測(cè)試案例。而其中的一些測(cè)試案例,對(duì)于關(guān)注功能的集成測(cè)試來說,可能是不同的等價(jià)類,但對(duì)關(guān)注schema的契約測(cè)試來說,則完全可能是相同的等價(jià)類,從而造成測(cè)試冗余。所以,合理的把握測(cè)試粒度,是非常重要的。當(dāng)然,就個(gè)人意見,我是反對(duì)這種和契約測(cè)試綁定的集成測(cè)試的。功能測(cè)試和契約測(cè)試,是完全不同的測(cè)試活動(dòng),它們肩負(fù)各自的使命、體現(xiàn)各自的價(jià)值,應(yīng)該各司其職。這是我和Beth Skurrie(Pact最主要的核心開發(fā)成員,沒有之一)多次探討的一致共識(shí)。
測(cè)試范疇,是另一個(gè)需要考慮的問題。上面提到過,Pact將契約測(cè)試的范疇定義在了communicate。什么是communicate呢?很簡(jiǎn)單,通過通訊獲取信息。具體到契約測(cè)試中,(可)通訊,體現(xiàn)在API的endpoint接受request(request包括protocol,url,header,body等),返回response;(可)獲取信息,體現(xiàn)在獲取的response能夠被按照期望的方式解析(反序列化)。需要強(qiáng)調(diào)的是,communicate的內(nèi)容不應(yīng)該包含"使用信息"。使用信息,是consumer的領(lǐng)域邏輯需要處理的問題,而信息使用得是否正確,則應(yīng)該是consumer的功能測(cè)試關(guān)注的范疇。注意,這里的功能測(cè)試可以發(fā)生在單元測(cè)試、組件測(cè)試、集成測(cè)試等各個(gè)測(cè)試級(jí)別。這就是為什么Pact的官方示例文檔中,在consumer端,僅僅斷言了response的status code這些非常簡(jiǎn)單的數(shù)據(jù)。如果consumer團(tuán)隊(duì)確實(shí)有需求,跨出communicate的范疇來構(gòu)建集成測(cè)試,那么請(qǐng)一定合理斟酌你們的測(cè)試范疇。

"生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試"?
相較于到目前為止通篇強(qiáng)調(diào)的"消費(fèi)者驅(qū)動(dòng)的契約測(cè)試",你可能在其他地方,或多或少的,看到過"生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試"的命題。
單論契約,確實(shí)可以分為"消費(fèi)者驅(qū)動(dòng)的契約"和"生產(chǎn)者驅(qū)動(dòng)的契約",但述及契約測(cè)試,到目前為止,恕俺視野有限,我并不認(rèn)為"生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試"是一種正確的表述。
契約不等于契約測(cè)試,這不必贅述;無論是消費(fèi)者驅(qū)動(dòng)、還是生產(chǎn)者驅(qū)動(dòng),其實(shí)質(zhì)一定都必須是契約測(cè)試。這點(diǎn),消費(fèi)者驅(qū)動(dòng)的契約測(cè)試不必多說,但對(duì)于"生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試",事實(shí)可能并不是這樣。生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試,其實(shí)質(zhì),就是上面討論過的
基于契約的測(cè)試(契約驅(qū)動(dòng)的測(cè)試);具體來說,生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試,強(qiáng)調(diào)的是,當(dāng)provider有需求和計(jì)劃更新既有服務(wù)的schema時(shí),在實(shí)際部署變更之前,先更新相應(yīng)的"契約"(為什么這里的契約要加引號(hào),自己琢磨琢磨),新的"契約",如果包含契約破壞,會(huì)導(dǎo)致consumer端的(契約驅(qū)動(dòng)的集成)測(cè)試掛掉。由此,consumer端可以在provider端真正部署包含契約破壞的服務(wù)之前,獲得預(yù)警,從而對(duì)consumer做必要的更新準(zhǔn)備,來適配provider將會(huì)部署上線的更新內(nèi)容;
在我看來,這是契約測(cè)試的一種反模式。在消費(fèi)者驅(qū)動(dòng)的契約測(cè)試中,契約是復(fù)數(shù)存在的,每一份契約都會(huì)被provider測(cè)試,如果有契約破壞,會(huì)被及時(shí)反饋,必要的時(shí)候會(huì)被修正;而"生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試"中,"契約"是唯一存在的,它的正確性是不會(huì)被測(cè)試和質(zhì)疑的,它僅僅會(huì)被consumer用來驗(yàn)證自己能否正確消費(fèi)這份"契約",所以,"生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試",測(cè)試的并不是"契約",而是consumer。
“如果質(zhì)疑生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試,是因?yàn)樗鼫y(cè)試的不是契約,而是consumer,那么是否也可以質(zhì)疑消費(fèi)者驅(qū)動(dòng)的契約測(cè)試,測(cè)試的也不是契約,而是provider呢?” 形式上來看,好像確實(shí)如此。但如果我們進(jìn)一步分析,不難發(fā)現(xiàn),消費(fèi)者驅(qū)動(dòng)的契約測(cè)試,對(duì)于不可接受的契約破壞的最終結(jié)果,要么是provider自主的功能修改被駁回,要么就是consumer主張的契約變更被駁回。結(jié)論就是,消費(fèi)者驅(qū)動(dòng)的契約測(cè)試,是對(duì)契約的雙方進(jìn)行約束,這體現(xiàn)了契約的意義,另一方面,對(duì)于不可接受的契約破壞,無論是哪一方引入的,它都將會(huì)被駁回,這體現(xiàn)了測(cè)試的意義(任何“功能”,如果交付測(cè)試后,無論結(jié)果好壞,它都是不可逆的,那測(cè)試本身也就失去了意義)。再來看“生產(chǎn)者驅(qū)動(dòng)的契約測(cè)試”,一旦provider發(fā)布了“契約”,無論是否發(fā)生(對(duì)任一consumer)不可接受的契約破壞,無論“測(cè)試”的結(jié)果如何,這份“契約”都不可能被駁回,這樣的“測(cè)試”,如果還說它的測(cè)試對(duì)象是“契約”的話,那這種“測(cè)試”對(duì)契約來說是沒有意義的。歸根到底,還是"契約測(cè)試"和"基于契約的測(cè)試(契約驅(qū)動(dòng)的測(cè)試)"的區(qū)別。
當(dāng)然,這樣的測(cè)試活動(dòng),并不是一無是處,在一些上下游非常不穩(wěn)定的微服務(wù)集群中,特別是在一些服務(wù)集群跨部門,甚至跨公司的多團(tuán)隊(duì)合作項(xiàng)目中,由于缺乏及時(shí)有效的溝通,往往更容易造成這樣那樣的契約破壞,此時(shí),這種基于契約的測(cè)試活動(dòng),能很好的預(yù)警provider的API schema變更對(duì)consumer的影響,這是非常有意義的。
最后
關(guān)于契約測(cè)試本身,和契約測(cè)試實(shí)施的問題,我想,遠(yuǎn)不止上面訴及的方面。不同的人、不同的團(tuán)隊(duì),對(duì)契約測(cè)試的理解也可能都不一樣,特別是,當(dāng)一種(比較)新的理念在不同的現(xiàn)實(shí)項(xiàng)目中付諸實(shí)踐時(shí),可能遇到的問題,和思考的方式又會(huì)有所迥異,這些都是我們理解一種理念的正常途徑。
問題永遠(yuǎn)都是客觀存在的,但解題的思路卻可以千奇百怪。我們討論P(yáng)act、Swagger和Spring Cloud Contract,我們辯駁消費(fèi)者驅(qū)動(dòng)和生產(chǎn)者驅(qū)動(dòng),我們思考是先寫契約測(cè)試還是先寫功能測(cè)試,這些思想的碰撞越多,越能幫助我們?nèi)ニ伎?、理解和總結(jié),繼而產(chǎn)生出更加富有想象力的答案。比如,當(dāng)需要把provider的schema中的一個(gè)String改成Object,從契約的角度,我們還在糾結(jié)如何協(xié)調(diào)所有的consumer影響最小時(shí),“聰明”的小伙伴已經(jīng)給出了這樣一個(gè)答案:不把String改成Object,而是直接添加這個(gè)Object。
最后,送上我經(jīng)常講的一個(gè)問題:你知道最簡(jiǎn)單靠譜的契約測(cè)試工具是什么嗎?是郵箱!╮( ̄▽ ̄)╭