契約測試(Contract testing)是一種測試技術,它通過以隔離檢查集成點上的每個應用的方式,確保應用發送或接收的消息符合調用雙方共識,并允許隨著時間的推移進行演化。
契約測試是對單元測試的增強,針對服務接口provider測試,覆蓋了一部分本來需要集成測試才能測試到的場景。
契約測試主要解決在存在溝通邊界情況下,測試替身(Test Double)與生產代碼表現可能不一致的問題。在契約測試中,契約由代碼生成,保持與現實同步,而且應用可以獨立于其它應用而僅基于契約進行快速測試。
由于集成測試容易受到網絡緩慢或不可靠,以及服務不可靠等因素的影響而運行緩慢或失敗,所以通常會引入測試替身來代替真實外部服務,以快速完成覆蓋度更廣的測試,讓測試真正起到作用。
金字塔模型是構建健康、快速、可維護測試集的成熟理論。
眾所周知,越是在項目生命周期的后期發現Bug,其修復的成本就越高。
不同于端到端(E2E)測試,契約測試可以在開發人員推送代碼之前運行,在開發階段提早發現問題。
契約測試還有很多端到端測試不具備的好處:
引入契約測試,還會帶來如下福利:
沒有兩個團隊是完全一樣的,契約測試也不是萬能的,關鍵要看契約測試可以為團隊和項目帶來什么。
契約測試可以用于任何需要通信的兩個服務,比如Web前端與后端API服務。
在微服務架構體系中,因為存在更多團隊獨立、服務間調用及服務單獨演進的情形,契約測試有了更好更大的用武之地。良好的契約測試,使得開發人員很容易避免版本地獄,是微服務開發和部署的利器。
契約測試主要涉及如下概念術語:
契約測試分為消費者驅動(consumer-driven)和提供者驅動(Provider-driven)兩種模式。
消費者驅動更具哲學意義,將API的消費者置于設計過程的核心,來倡導更好的內部微服務設計。該模式的優點在于,只有消費者正在使用的部分會得到測試,而提供者可以自由地更改消費者不使用的任何其它部分,而不必破壞任何現有測試。
提供者驅動思路較為常規,更適合開放數據或系統的場景。
無論采用哪種風格,關鍵在于獲得契約測試的好處,實現引入契約測試的目的。
消費者驅動的契約測試運行步驟如下:
提供者驅動模式由提供者定義契約并驅動整個過程。
流行的契約測試工具為:
9.利用Pact進行消費者驅動的測試價值
利用Pact進行契約測試的整個流程示意如下,使用了 pact 之后,依然是每個服務獨立的進行單元測試,但是可以模擬出真實集成場景。
pact契約測試分為兩步:
以下為nlp-pact-parent示例:
<dependencies><!-- contract testing --><dependency> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-consumer-junit5</artifactId> <version>4.0.10</version> <scope>test</scope></dependency><dependency> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-provider-junit5</artifactId> <version>4.0.10</version> <scope>test</scope></dependency>
編寫ConsumerTest生成契約:
@ExtendWith(PactConsumerTestExt.class)@SpringBootTest(classes = PactConsumerApplication.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)@PactTestFor(providerName = "nlp-pact-provider", port = "8202")public class ConsumerTest { private TestRestTemplate restTemplate = new TestRestTemplate(); @Test @PactTestFor(pactMethod = "greetingPact") void greeting_shouldReturnMessage() { // Arrange HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json"); // Act ResponseEntity<Map> response = restTemplate.getForEntity("http://localhost:8202/greeting?name=John", Map.class); // Assert assertEquals(HttpStatus.OK, response.getStatusCode()); assertEquals(Collections.singletonMap("message", "Hello, John!"), response.getBody()); } // Pact 定義 @Pact(consumer = "nlp-pact-consumer", provider = "nlp-pact-provider") public RequestResponsePact greetingPact(PactDslWithProvider builder) { return builder .given("a request for greeting with name 'John'") .uponReceiving("a request to greet John") .path("/greeting") .method("GET") .query("name=John") .willRespondWith() .status(200) .headers(Collections.singletonMap("Content-Type", "application/json")) .body("{/"message/": /"Hello, John!/"}") .toPact(); }}
# nlp-pact-consumer-nlp-pact-provider.json{ "provider": { "name": "nlp-pact-provider" }, "consumer": { "name": "nlp-pact-consumer" }, "interactions": [ { "description": "a request to greet John", "request": { "method": "GET", "path": "/greeting", "query": { "name": [ "John" ] } }, "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "body": { "message": "Hello, John!" } }, "providerStates": [ { "name": "a request for greeting with name 'John'" } ] } ], "metadata": { "pactSpecification": { "version": "3.0.0" }, "pact-jvm": { "version": "4.0.10" } }}
build配置如下:
<plugin> <groupId>au.com.dius</groupId> <artifactId>pact-jvm-provider-maven</artifactId> <version>4.0.0</version> <configuration> <serviceProviders> <!-- You can define as many as you need, but each must have a unique name --> <serviceProvider> <name>nlp-pact-provider</name> <!-- All the provider properties are optional, and have sensible defaults (shown below) --> <protocol>http</protocol> <host>localhost</host> <port>8200</port> <path>/</path> <pactFileDirectory>resources/pacts</pactFileDirectory> </serviceProvider> </serviceProviders> <pactBrokerUrl/> </configuration></plugin>
Found 1 pact filesVerifying a pact between nlp-pact-consumer and nlp-pact-provider [Using File D:/IdeaProjects/nlp-other-project-dev/nlp-pact-parent/nlp-pact-provider/target/pacts/nlp-pact-consumer-nlp-pact-provider.json] Given a request for greeting with name 'John' WARNING: State Change ignored as there is no stateChange URL a request to greet John returns a response which has status code 200 (OK) has a matching body (OK)[WARNING] Skipping publishing of verification results as it has been disabled (pact.verifier.publishResults is not 'true')[INFO] ------------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] ------------------------------------------------------------------------[INFO] Total time: 1.589 s[INFO] Finished at: 2023-03-08T15:02:33+08:00[INFO] --------
Pact Broker是一個用于共享消費者驅動的合同和驗證結果的應用程序。
pact主頁面:
查看服務間關系:
與CICD集成:
我在不少項目中都嘗試過實施契約測試,但是真正實施成功的并不多,主要原因還是規模和痛點不夠大,從而導致團隊覺得沒有必要做,或者覺得做了收益比投入少。而成功的一般的都是團隊人員足夠痛,或者經歷過大型多團隊項目中服務改變等各種痛點,從而導致他們解決自己的痛點而主動實施契約測試,但是前提是他們都知道契約測試。所以要成功實施契約都是有兩個主要的前提條件:1,團隊對于相關問題足夠痛,2,團隊懂契約測試。在這種情況下,團隊才可能愿意主動實施契約測試,才能成功的實施契約測試。所以首先是要讓開發團隊懂契約測試,比如契約測試能解決什么問題,實施流程,相關測試框架等,然后等待團隊無法忍受相關痛點后,成功的實施契約測試就可以水到渠成了。
本文鏈接:http://www.www897cc.com/showinfo-26-78655-0.html探秘Spring Contract:如何保障您的API符合預期?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com