消費(fèi)者驅(qū)動的契約測試 Spring Cloud Contract介紹

什么是契約測試

測試是軟件流程中非常重要,不可或缺的一個環(huán)節(jié)。一般的測試分為單元測試召耘,集成測試,端到端的手工測試褐隆,這也是構(gòu)成測試金字塔的三個層級污它。我們今天將要討論的話題是契約測試,它是處于單元測試和集成測試中間的一個環(huán)節(jié)妓灌。這三個層級分別測試的場景如下:

  • 單元測試:測試單個service
  • 集成測試:測試由多個services組成的系統(tǒng)
  • 端到端測試:測試從用戶到各個外部系統(tǒng)的整個場景

契約測試的作用:

  • 測試接口和接口之間的正確性
  • 驗(yàn)證服務(wù)層提供的數(shù)據(jù)是否是消費(fèi)端所需要的
  • 將本來需要在集成測試中體現(xiàn)的問題前移轨蛤,更早的發(fā)現(xiàn)問題
  • 更快速的驗(yàn)證消費(fèi)端和提供端之間交互的基本正確性

為什么要存在契約測試

首先我們將使用以下示例模型來描述微服務(wù)測試背后的概念:

圖1

在上面的圖中,我們可以看到有兩個微服務(wù)虫埂,通過REST彼此進(jìn)行通信。第一項(xiàng)服務(wù)扮演消費(fèi)者的角色圃验,第二項(xiàng)扮演提供者的角色掉伏。

當(dāng)需要進(jìn)行集成測試時,可以通過服務(wù)虛擬化來模擬正在與之通信的微服務(wù)。這里服務(wù)提供者被模擬斧散,在部署消費(fèi)者服務(wù)之前供常,您希望證明其能正常工作。當(dāng)運(yùn)行所有測試均為綠色您認(rèn)為可以部署您的服務(wù)了鸡捐。

圖2

但是栈暇,如果您針對生產(chǎn)提供商運(yùn)行服務(wù),而不是模擬版本箍镜,則有可能會失敗源祈。在這個例子中,提供者已經(jīng)改變了數(shù)據(jù)格式色迂。集成測試無法解決這個問題香缺,因?yàn)樗鼈冋卺槍rovider的過時版本運(yùn)行。

圖3

如何填補(bǔ)測試過程中的這個空白歇僧?將引入消費(fèi)者驅(qū)動契約測試的概念图张。消費(fèi)者驅(qū)動契約測試方法是在消費(fèi)者和提供者之間定義在它們彼此之間轉(zhuǎn)移的數(shù)據(jù)格式。通常诈悍,合同的格式由消費(fèi)者定義并與相應(yīng)的提供商共享祸轮。之后,執(zhí)行測試以驗(yàn)證契約是否相符侥钳。CDC測試的先決條件之一是可以與提供商服務(wù)團(tuán)隊(duì)保持良好的最佳密切溝通倔撞,分享這些契約和交流測試結(jié)果是實(shí)施適當(dāng)?shù)腃DC測試的重要部分。

PACT測試框架

PACT是一個開源的CDC測試框架慕趴。它提供了廣泛的語言支持痪蝇,如Ruby,Java冕房,Scala躏啰,.NET,Javascript耙册,Swift/Objective-C给僵。

PACT的工作原理

消費(fèi)者作為數(shù)據(jù)的最終使用者非常清楚、明確的知道需要的什么樣格式详拙,什么類型的數(shù)據(jù)帝际,它將負(fù)責(zé)創(chuàng)建契約文檔(包含結(jié)構(gòu)和格式的json文件),服務(wù)提供端將根據(jù)消費(fèi)者端創(chuàng)建的契約文檔提供對應(yīng)格式的數(shù)據(jù)并返回給消費(fèi)者饶辙,通過契約檢查判斷如果服務(wù)端提供的數(shù)據(jù)和消費(fèi)者生成的契約不匹配蹲诀,將拋出異常并提示給服務(wù)提供端。

Spring Cloud Contract

Spring Cloud Contract是一個基于消費(fèi)者驅(qū)動契約的測試框架弃揽。它會基于契約來生成存根服務(wù)脯爪,消費(fèi)方不需要等待接口開發(fā)完成则北,就可以通過存根服務(wù)完成集成測試。Spring Could Contract中痕慢,契約是用一種基于 Groovy 的 DSL 定義的尚揣。

談到契約測試時,我們首先需要定義一個包含期望使用接口的第一個文件掖举。作為標(biāo)準(zhǔn)PACT法則快骗,契約必須由消費(fèi)者服務(wù)來定義,但是在Spring Cloud Contract中塔次,它實(shí)際上位于提供者服務(wù)代碼中方篮。在指南手冊中包含了兩個大步驟:

服務(wù)提供者

  1. 編寫合同規(guī)范(Groovy DSL)
  2. 在Provider端生成自動驗(yàn)收測試
  3. 生成WireMock JSON存根&將存根發(fā)布到Maven(本地)存儲庫

服務(wù)消費(fèi)者

  1. 在消費(fèi)者端配置Stub Runner
  2. 執(zhí)行消費(fèi)者測試 - Stub Runner嵌入了WireMock
  3. 檢查驗(yàn)證結(jié)果

服務(wù)提供者

我們在服務(wù)端編寫一個簡單服務(wù)接口,判斷數(shù)字是奇數(shù)還是偶數(shù)

@RestController
public class EvenOddController {
    @GetMapping("/validate/prime-number")
    public String isNumberPrime(@RequestParam("number") Integer number) {
        return number % 2 == 0 ? "Even" : "Odd";
    }
}

MAVEN 依賴

對于我們的提供者俺叭,我們需要spring-cloud-starter-contract-verifier依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
</dependency>

需要將我們的基礎(chǔ)測試類的名稱配置到spring-cloud-contract-maven-plugin:

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>1.2.2.RELEASE</version>
    <extensions>true</extensions>
    <configuration>
        <baseClassForTests>com.peterwanghao.spring.cloud.contract.producer.BaseTestClass
        </baseClassForTests>
    </configuration>
</plugin>

基礎(chǔ)測試類

需要在加載Spring上下文的測試包中添加一個基類:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@DirtiesContext
@AutoConfigureMessageVerifier
public class BaseTestClass {
    @Autowired
    private EvenOddController evenOddController;

    @Before
    public void setup() {
        StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(evenOddController);
        RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
    }
}

測試存根

在/src/test/ resources/contracts/目錄中恭取,我們將在groovy文件中添加測試存根。例如

import org.springframework.cloud.contract.spec.Contract

Contract.make {
    description "should return even when number input is even"
    request {
        method GET()
        url("/validate/prime-number") {
            queryParameters {
                parameter("number", "2")
            }
        }
    }
    response {
        body("Even")
        status 200
    }
}

當(dāng)我們運(yùn)行構(gòu)建時熄守,運(yùn)行 mvn clean install 插件會自動生成一個名為ContractVerifierTest的測試類蜈垮,它擴(kuò)展我們的BaseTestClass并將其放在/target/generated-test-sources/contracts/中。

測試方法的名稱派生自前綴“ validate_”與我們的Groovy測試存根的名稱連接裕照。對于上面的Groovy文件攒发,生成的方法名稱將為“validate_shouldReturnEvenWhenRequestParamIsEven”。

我們來看看這個自動生成的測試類:

public class ContractVerifierTest extends BaseTestClass {

    @Test
    public void validate_shouldReturnEvenWhenRequestParamIsEven() throws Exception {
        // given:
            MockMvcRequestSpecification request = given();

        // when:
            ResponseOptions response = given().spec(request)
                    .queryParam("number","2")
                    .get("/validate/prime-number");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
        // and:
            String responseBody = response.getBody().asString();
            assertThat(responseBody).isEqualTo("Even");
    }
}

構(gòu)建還將在我們的本地Maven存儲庫中添加存根jar晋南,以便我們的消費(fèi)者可以使用它惠猿。

服務(wù)消費(fèi)者

我們的CDC消費(fèi)者將通過HTTP交互生成的存根來維護(hù)契約,因此提供者方面的任何更改都將破壞契約负间。

新建BasicMathController偶妖,它將發(fā)出HTTP請求以從生成的存根中獲取響應(yīng):

@RestController
public class BasicMathController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/calculate")
    public String checkOddAndEven(@RequestParam("number") Integer number) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("Content-Type", "application/json");

        ResponseEntity<String> responseEntity = restTemplate.exchange(
                "http://localhost:8090/validate/prime-number?number=" + number, HttpMethod.GET,
                new HttpEntity<>(httpHeaders), String.class);

        return responseEntity.getBody();
    }
}

MAVEN 依賴

對于我們的消費(fèi)者,我們需要添加spring-cloud-contract-wiremock和spring-cloud-contract-stub-runner依賴項(xiàng)政溃。還有本地Maven存儲庫中的可用存根:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-wiremock</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-stub-runner</artifactId>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>com.peterwanghao.spring.cloud</groupId>
    <artifactId>spring-cloud-contract-producer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>test</scope>
</dependency>

存根運(yùn)行器

現(xiàn)在是時候配置我們的存根運(yùn)行器趾访,它將通知我們的消費(fèi)者如何調(diào)用我們本地Maven存儲庫中的可用存根:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner(workOffline = true, ids = "com.peterwanghao.spring.cloud:spring-cloud-contract-producer:+:stubs:8090")
public class BasicMathControllerIntegrationTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void given_WhenPassEvenNumberInQueryParam_ThenReturnEven() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/calculate?number=2").contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andExpect(content().string("Even"));
    }

}

通過@AutoConfigureStubRunner自動注入StubRunner,模擬服務(wù)方。

參數(shù)ids定位到maven中的stub.jar董虱。

Ids = groupId : artifactId : version(’+’表示最新版本): 存根 : StubRunner端口

如果你將stub.jar發(fā)布到Maven私服中扼鞋,可以通過repositoryRoot參數(shù)指定私服地址來遠(yuǎn)程調(diào)用。在測試通過后會根據(jù)契約返回響應(yīng)內(nèi)容愤诱。

總結(jié)

文中首先介紹了契約測試的背景以及基于CDC開發(fā)服務(wù)的大致過程云头。然后編寫契約文件通過Spring Cloud Contract的contract verifier插件生成存根和服務(wù)提供方的測試用例,消費(fèi)方編寫測試用例淫半,通過StrubRunner模擬服務(wù)方來完成一次消費(fèi)方調(diào)用服務(wù)方的測試溃槐。

本文包含的代碼地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市撮慨,隨后出現(xiàn)的幾起案子竿痰,更是在濱河造成了極大的恐慌脆粥,老刑警劉巖砌溺,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件影涉,死亡現(xiàn)場離奇詭異,居然都是意外死亡规伐,警方通過查閱死者的電腦和手機(jī)蟹倾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來猖闪,“玉大人鲜棠,你說我怎么就攤上這事∨嗷牛” “怎么了豁陆?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吵护。 經(jīng)常有香客問我盒音,道長,這世上最難降的妖魔是什么馅而? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任祥诽,我火速辦了婚禮,結(jié)果婚禮上瓮恭,老公的妹妹穿的比我還像新娘雄坪。我一直安慰自己,他們只是感情好屯蹦,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布维哈。 她就那樣靜靜地躺著,像睡著了一般登澜。 火紅的嫁衣襯著肌膚如雪阔挠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天帖渠,我揣著相機(jī)與錄音谒亦,去河邊找鬼。 笑死空郊,一個胖子當(dāng)著我的面吹牛份招,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播狞甚,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼锁摔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哼审?” 一聲冷哼從身側(cè)響起谐腰,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤孕豹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后十气,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體励背,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年砸西,在試婚紗的時候發(fā)現(xiàn)自己被綠了叶眉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡芹枷,死狀恐怖衅疙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鸳慈,我是刑警寧澤饱溢,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站走芋,受9級特大地震影響绩郎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绿聘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一嗽上、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熄攘,春花似錦兽愤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哲思,卻和暖如春洼畅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背棚赔。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工帝簇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人靠益。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓丧肴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親胧后。 傳聞我的和親對象是個殘疾皇子芋浮,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容