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

什么是契約測試

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

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

契約測試的作用:

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

為什么要存在契約測試

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

圖1

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

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

圖2

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

圖3

如何填補(bǔ)測試過程中的這個(gè)空白?將引入消費(fèi)者驅(qū)動(dòng)契約測試的概念。消費(fèi)者驅(qū)動(dòng)契約測試方法是在消費(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是一個(gè)開源的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)建的契約文檔提供對(duì)應(yīng)格式的數(shù)據(jù)并返回給消費(fèi)者,通過契約檢查判斷如果服務(wù)端提供的數(shù)據(jù)和消費(fèi)者生成的契約不匹配,將拋出異常并提示給服務(wù)提供端。

Spring Cloud Contract

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

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

服務(wù)提供者

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

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

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

服務(wù)提供者

我們?cè)诜?wù)端編寫一個(gè)簡單服務(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 依賴

對(duì)于我們的提供者,我們需要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上下文的測試包中添加一個(gè)基類:

@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)建時(shí),運(yùn)行 mvn clean install 插件會(huì)自動(dòng)生成一個(gè)名為ContractVerifierTest的測試類,它擴(kuò)展我們的BaseTestClass并將其放在/target/generated-test-sources/contracts/中。

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

我們來看看這個(gè)自動(dòng)生成的測試類:

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存儲(chǔ)庫中添加存根jar,以便我們的消費(fèi)者可以使用它。

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

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

新建BasicMathController,它將發(fā)出HTTP請(qǐng)求以從生成的存根中獲取響應(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 依賴

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

<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)在是時(shí)候配置我們的存根運(yùn)行器,它將通知我們的消費(fèi)者如何調(diào)用我們本地Maven存儲(chǔ)庫中的可用存根:

@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自動(dòng)注入StubRunner,模擬服務(wù)方。

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

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

如果你將stub.jar發(fā)布到Maven私服中,可以通過repositoryRoot參數(shù)指定私服地址來遠(yuǎn)程調(diào)用。在測試通過后會(huì)根據(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)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容