從零開始學習 Consumer-Driven Contract Testing(消費者驅動的合約測試)
- 什麼是 Spring Cloud Contract?
- 為什麼需要合約測試?
- 核心概念
- 專案架構總覽
- Producer 端實作教學
- Consumer 端實作教學
- 合約 DSL 語法詳解
- YAML 合約格式
- 進階功能
- 訊息驅動合約
- 常見問題與解決方案
- 最佳實踐
- 完整工作流程
Spring Cloud Contract 是一個支援 Consumer-Driven Contract(CDC) 測試的框架。它確保微服務之間的 API 溝通符合雙方預期,避免在整合時出現問題。
┌──────────────┐ 合約 (Contract) ┌──────────────┐
│ Consumer │ ◄──────────────────────► │ Producer │
│ (消費者端) │ 定義 API 互動的規範 │ (提供者端) │
└──────────────┘ └──────────────┘
│ │
│ 使用 Stub 進行測試 │ 根據合約自動產生測試
│ (不需要啟動真實 Producer) │ (確保 API 符合合約)
▼ ▼
┌──────────┐ ┌──────────────┐
│ WireMock │ │ 自動產生的測試 │
│ Stub │ │ (驗證 API) │
└──────────┘ └──────────────┘
簡單來說:
- 合約(Contract):Producer 和 Consumer 之間的 API 協議
- Producer:提供 API 的服務,需要滿足合約
- Consumer:呼叫 API 的服務,依賴合約來進行測試
| 問題 | 說明 |
|---|---|
| 環境依賴 | 需要所有服務同時運行才能測試 |
| 測試緩慢 | 啟動多個服務非常耗時 |
| 難以定位問題 | 錯誤可能來自任何一方 |
| 脆弱的測試 | 一個服務的改動可能破壞所有相關測試 |
| 優勢 | 說明 |
|---|---|
| 獨立測試 | Producer 和 Consumer 可以各自獨立測試 |
| 快速反饋 | 不需要啟動整個系統 |
| 清晰的責任 | 合約明確定義了雙方的責任 |
| 自動化驗證 | 合約變更時,雙方都能自動發現不相容的改動 |
合約定義了 Producer 和 Consumer 之間 API 交互的規範,包括:
- Request:HTTP 方法、URL、Headers、Body
- Response:狀態碼、Headers、Body
從合約自動產生的模擬服務(基於 WireMock),讓 Consumer 可以在不啟動真實 Producer 的情況下進行測試。
Spring Cloud Contract 會根據合約自動產生 Producer 端的測試類別,確保 Producer 的 API 實作符合合約。
1. 定義合約(通常放在 Producer 端)
│
▼
2. Producer 端:
- Maven Plugin 根據合約自動產生測試
- 執行測試,確保 API 實作符合合約
- 產生 Stub JAR 並安裝到 Maven 倉庫
│
▼
3. Consumer 端:
- 使用 Stub Runner 載入 Stub JAR
- 自動啟動 WireMock 伺服器
- 對 WireMock 進行測試(模擬真實 Producer)
spring-cloud-contract-tutorial/
│
├── producer/ # Producer 端專案
│ ├── pom.xml # Maven 設定(含 Contract Plugin)
│ ├── src/main/java/
│ │ └── com/example/producer/
│ │ ├── ProducerApplication.java
│ │ ├── controller/
│ │ │ ├── UserController.java # User REST API
│ │ │ └── OrderController.java # Order REST API
│ │ ├── model/
│ │ │ ├── User.java
│ │ │ └── Order.java
│ │ └── service/
│ │ ├── UserService.java
│ │ └── OrderService.java
│ └── src/test/
│ ├── java/com/example/producer/
│ │ ├── BaseContractTest.java # 預設基底測試類別
│ │ ├── UserBaseContractTest.java # User 合約基底類別
│ │ └── OrderBaseContractTest.java # Order 合約基底類別
│ └── resources/contracts/
│ ├── user/ # User 相關合約
│ │ ├── shouldReturnUserById.groovy
│ │ ├── shouldReturnAllUsers.groovy
│ │ ├── shouldCreateNewUser.groovy
│ │ ├── shouldUpdateUser.groovy
│ │ ├── shouldDeleteUser.groovy
│ │ └── shouldReturnNotFoundForMissingUser.groovy
│ └── order/ # Order 相關合約
│ ├── shouldReturnOrderById.groovy
│ ├── shouldReturnOrdersByUserId.groovy
│ ├── shouldCreateOrder.groovy
│ └── shouldUpdateOrderStatus.groovy
│
├── consumer/ # Consumer 端專案
│ ├── pom.xml # Maven 設定(含 Stub Runner)
│ ├── src/main/java/
│ │ └── com/example/consumer/
│ │ ├── ConsumerApplication.java
│ │ ├── controller/
│ │ │ └── ConsumerController.java
│ │ ├── model/
│ │ │ ├── User.java
│ │ │ └── Order.java
│ │ ├── service/
│ │ │ ├── UserServiceClient.java # 呼叫 Producer API
│ │ │ └── OrderServiceClient.java
│ │ └── config/
│ │ └── RestTemplateConfig.java
│ └── src/test/java/
│ └── com/example/consumer/
│ ├── UserServiceClientContractTest.java
│ └── OrderServiceClientContractTest.java
│
└── advanced-examples/ # 進階範例
├── messaging-producer/ # 訊息驅動合約範例
└── yaml-contracts/ # YAML 格式合約範例
Producer 端需要加入 spring-cloud-contract-maven-plugin 和 spring-cloud-starter-contract-verifier。
pom.xml 關鍵設定:
<!-- 依賴:合約驗證器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<!-- Plugin:自動產生測試和 Stub -->
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<!-- 指定預設的基底測試類別 -->
<baseClassForTests>
com.example.producer.BaseContractTest
</baseClassForTests>
<!-- 按目錄映射不同的基底類別 -->
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*user.*</contractPackageRegex>
<baseClassFQN>com.example.producer.UserBaseContractTest</baseClassFQN>
</baseClassMapping>
<baseClassMapping>
<contractPackageRegex>.*order.*</contractPackageRegex>
<baseClassFQN>com.example.producer.OrderBaseContractTest</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
</configuration>
</plugin>以 UserController 為例:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
User created = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// ... 其他方法
}合約放在 src/test/resources/contracts/ 目錄下。
範例 1:基本 GET 請求合約
// shouldReturnUserById.groovy
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description "應該根據 ID 回傳使用者資料"
request {
method GET()
url "/api/users/1"
headers {
contentType applicationJson()
}
}
response {
status OK() // 200
headers {
contentType applicationJson()
}
body(
id: 1,
name: "Alice",
email: "alice@example.com",
age: 28
)
}
}範例 2:POST 請求合約
// shouldCreateNewUser.groovy
Contract.make {
description "應該成功建立新使用者並回傳 201"
request {
method POST()
url "/api/users"
headers {
contentType applicationJson()
}
body(
name: "David",
email: "david@example.com",
age: 30
)
}
response {
status CREATED() // 201
headers {
contentType applicationJson()
}
body(
// producer() 端使用 regex 做彈性匹配
// consumer() 端使用固定值產生 stub
id: $(producer(regex(positiveInt())), consumer(4)),
name: "David",
email: "david@example.com",
age: 30
)
}
}範例 3:錯誤情境合約(404)
// shouldReturnNotFoundForMissingUser.groovy
Contract.make {
description "查詢不存在的使用者應回傳 404"
request {
method GET()
url "/api/users/999"
}
response {
status NOT_FOUND() // 404
}
}範例 4:DELETE 請求合約
// shouldDeleteUser.groovy
Contract.make {
description "應該成功刪除使用者並回傳 204"
request {
method DELETE()
url "/api/users/1"
}
response {
status NO_CONTENT() // 204
}
}範例 5:PATCH 請求合約
// shouldUpdateOrderStatus.groovy
Contract.make {
description "應該成功更新訂單狀態"
request {
method PATCH()
url "/api/orders/1/status"
headers {
contentType applicationJson()
}
body(
status: "SHIPPED"
)
}
response {
status OK()
headers {
contentType applicationJson()
}
body(
id: 1,
status: "SHIPPED"
// ... 其他欄位
)
}
}Spring Cloud Contract 自動產生的測試需要繼承一個基底類別:
@SpringBootTest
public abstract class UserBaseContractTest {
@Autowired
private UserController userController;
@BeforeEach
void setup() {
// 設定 RestAssuredMockMvc,用來模擬 HTTP 請求
RestAssuredMockMvc.standaloneSetup(userController);
}
}# 進入 producer 目錄
cd producer
# 執行測試 + 產生 Stub JAR
mvn clean install
# 執行後會:
# 1. 在 target/generated-test-sources/ 產生測試類別
# 2. 執行自動產生的測試
# 3. 產生 Stub JAR 並安裝到本地 Maven 倉庫自動產生的測試類別(示意):
// 自動產生,不需手動撰寫
public class UserTest extends UserBaseContractTest {
@Test
public void validate_shouldReturnUserById() throws Exception {
// given:
MockMvcRequestSpecification request = given()
.header("Content-Type", "application/json");
// when:
ResponseOptions response = given().spec(request)
.get("/api/users/1");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['id']").isEqualTo(1);
assertThatJson(parsedJson).field("['name']").isEqualTo("Alice");
assertThatJson(parsedJson).field("['email']").isEqualTo("alice@example.com");
assertThatJson(parsedJson).field("['age']").isEqualTo(28);
}
}Consumer 端需要加入 spring-cloud-starter-contract-stub-runner:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>Consumer 使用 RestTemplate 呼叫 Producer API:
@Service
public class UserServiceClient {
private final RestTemplate restTemplate;
private final String baseUrl;
public UserServiceClient(RestTemplate restTemplate,
@Value("${producer.base-url}") String baseUrl) {
this.restTemplate = restTemplate;
this.baseUrl = baseUrl;
}
public Optional<User> getUserById(Long id) {
try {
User user = restTemplate.getForObject(
baseUrl + "/api/users/{id}", User.class, id);
return Optional.ofNullable(user);
} catch (HttpClientErrorException.NotFound e) {
return Optional.empty();
}
}
public User createUser(User user) {
return restTemplate.postForObject(
baseUrl + "/api/users", user, User.class);
}
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@AutoConfigureStubRunner(
// 格式:groupId:artifactId:version:classifier:port
// + 表示最新版本
ids = "com.example:contract-producer:+:stubs:8080",
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
public class UserServiceClientContractTest {
@Autowired
private UserServiceClient userServiceClient;
@Test
void shouldGetUserById() {
// Stub Runner 已經啟動了 WireMock,
// 載入了 Producer 的 Stub
Optional<User> user = userServiceClient.getUserById(1L);
assertThat(user).isPresent();
assertThat(user.get().getId()).isEqualTo(1L);
assertThat(user.get().getName()).isEqualTo("Alice");
assertThat(user.get().getEmail()).isEqualTo("alice@example.com");
}
@Test
void shouldReturnEmptyForMissingUser() {
Optional<User> user = userServiceClient.getUserById(999L);
assertThat(user).isEmpty();
}
@Test
void shouldCreateUser() {
User newUser = new User();
newUser.setName("David");
newUser.setEmail("david@example.com");
newUser.setAge(30);
User created = userServiceClient.createUser(newUser);
assertThat(created).isNotNull();
assertThat(created.getId()).isPositive();
assertThat(created.getName()).isEqualTo("David");
}
}| 參數 | 說明 |
|---|---|
ids |
Stub 的 Maven 座標,格式 groupId:artifactId:version:classifier:port |
stubsMode |
LOCAL(本地 Maven 倉庫)、REMOTE(遠端倉庫)、CLASSPATH(類別路徑) |
repositoryRoot |
遠端倉庫的 URL(REMOTE 模式使用) |
ids 格式範例:
com.example:contract-producer:+:stubs:8080
│ │ │ │ │
│ │ │ │ └── WireMock 啟動的 port
│ │ │ └── classifier(固定為 stubs)
│ │ └── 版本(+ 表示最新)
│ └── artifactId
└── groupId
# 前提:先在 Producer 端執行 mvn clean install
cd producer
mvn clean install
# 然後在 Consumer 端執行測試
cd ../consumer
mvn clean testrequest {
method GET() // GET 請求
method POST() // POST 請求
method PUT() // PUT 請求
method PATCH() // PATCH 請求
method DELETE() // DELETE 請求
}request {
// 固定 URL
url "/api/users/1"
// URL 帶查詢參數
url("/api/users") {
queryParameters {
parameter "page": 0
parameter "size": 10
parameter "sort": "name,asc"
}
}
// URL 使用正則表達式
urlPath $(consumer(regex("/api/users/[0-9]+")))
}request {
headers {
contentType applicationJson()
header "Authorization": "Bearer token123"
header "X-Custom-Header": $(consumer("value"), producer(regex(nonEmpty())))
}
}
response {
headers {
contentType applicationJson()
header "X-Request-Id": $(producer(regex(uuid())), consumer("550e8400-e29b-41d4-a716-446655440000"))
}
}request {
// 物件格式
body(
name: "David",
email: "david@example.com",
age: 30
)
// 巢狀物件
body(
user: [
name: "David",
address: [
city: "Taipei",
zip: "100"
]
]
)
// 從檔案讀取
body(file("request.json"))
}response {
body(
// 固定值
name: "Alice",
// Producer 端使用 regex,Consumer 端使用固定值
id: $(producer(regex(positiveInt())), consumer(1)),
// 簡寫
email: $(regex(email())),
// 自訂正則
status: $(regex("ACTIVE|INACTIVE|PENDING"))
)
}| 方法 | 匹配 | 範例 |
|---|---|---|
onlyAlphaUnicode() |
只有字母 | Alice |
alphaNumeric() |
字母和數字 | User123 |
number() |
數字(含小數) | 42, 3.14 |
positiveInt() |
正整數 | 1, 42 |
anyBoolean() |
布林值 | true, false |
ip() |
IP 位址 | 192.168.1.1 |
hostname() |
主機名 | example.com |
email() |
user@example.com |
|
url() |
URL | https://example.com |
uuid() |
UUID | 550e8400-... |
isoDate() |
ISO 日期 | 2024-01-01 |
isoDateTime() |
ISO 日期時間 | 2024-01-01T00:00:00 |
isoTime() |
ISO 時間 | 12:00:00 |
nonEmpty() |
非空字串 | anything |
nonBlank() |
非空白字串 | anything |
response {
status OK() // 200
status CREATED() // 201
status ACCEPTED() // 202
status NO_CONTENT() // 204
status MOVED_PERMANENTLY() // 301
status FOUND() // 302
status BAD_REQUEST() // 400
status UNAUTHORIZED() // 401
status FORBIDDEN() // 403
status NOT_FOUND() // 404
status CONFLICT() // 409
status TOO_MANY_REQUESTS() // 429
status INTERNAL_SERVER_ERROR() // 500
status SERVICE_UNAVAILABLE() // 503
status 200 // 直接用數字
}除了 Groovy DSL,也可以使用 YAML 格式來定義合約。適合不熟悉 Groovy 的團隊。
# shouldReturnUserById.yml
description: "應該根據 ID 回傳使用者資料"
request:
method: GET
url: /api/users/1
headers:
Content-Type: application/json
response:
status: 200
headers:
Content-Type: application/json
body:
id: 1
name: "Alice"
email: "alice@example.com"
age: 28# shouldCreateUser.yml
description: "應該成功建立新使用者"
request:
method: POST
url: /api/users
headers:
Content-Type: application/json
body:
name: "David"
email: "david@example.com"
age: 30
response:
status: 201
headers:
Content-Type: application/json
body:
id: 4
name: "David"
email: "david@example.com"
age: 30
matchers:
body:
- path: $.id
type: by_regex
value: "[0-9]+"| 特性 | Groovy DSL | YAML |
|---|---|---|
| 學習曲線 | 需要了解 Groovy | 直覺易懂 |
| 動態值 | $(producer(...), consumer(...)) |
matchers 區塊 |
| 複雜邏輯 | 支援(可寫 Groovy 程式碼) | 有限 |
| IDE 支援 | IntelliJ 有完整支援 | 基本語法高亮 |
| 推薦場景 | 複雜合約 | 簡單合約、跨語言團隊 |
body(
// producer() 側:用於 Producer 測試時的驗證規則
// consumer() 側:用於產生 Stub 時回傳的固定值
id: $(producer(regex(positiveInt())), consumer(42))
)白話解釋:
- 當 Producer 測試跑起來時:API 回傳的
id只要是正整數就通過 - 當 Consumer 使用 Stub 時:WireMock 會固定回傳
id: 42
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
description "取得使用者 - 成功"
request {
method GET()
url "/api/users/1"
}
response {
status OK()
body(id: 1, name: "Alice")
}
},
Contract.make {
description "取得使用者 - 不存在"
request {
method GET()
url "/api/users/999"
}
response {
status NOT_FOUND()
}
}
]當多個合約匹配同一個請求時,可以設定優先權:
Contract.make {
priority 1 // 數字越小優先權越高
request {
method GET()
url "/api/users/1"
}
response {
status OK()
body(id: 1, name: "Alice")
}
}Contract.make {
request {
method POST()
url "/api/users"
body(
name: "David",
email: "david@example.com"
)
bodyMatchers {
// 驗證 name 欄位符合正則
jsonPath('$.name', byRegex(nonEmpty()))
// 驗證 email 欄位符合 email 格式
jsonPath('$.email', byRegex(email()))
}
}
response {
status CREATED()
body(
id: 1,
name: "David"
)
bodyMatchers {
jsonPath('$.id', byRegex(positiveInt()))
}
}
}Contract.make {
request {
method POST()
url "/api/users"
headers {
contentType applicationJson()
}
// 從檔案讀取 request body
body(file("createUserRequest.json"))
}
response {
status CREATED()
headers {
contentType applicationJson()
}
// 從檔案讀取 response body
body(file("createUserResponse.json"))
}
}Contract.make {
description "分頁查詢使用者"
request {
method GET()
url("/api/users") {
queryParameters {
parameter "page": $(consumer(regex("[0-9]+")), producer("0"))
parameter "size": $(consumer(regex("[0-9]+")), producer("10"))
}
}
}
response {
status OK()
headers {
contentType applicationJson()
}
body(
content: [
[id: 1, name: "Alice"],
[id: 2, name: "Bob"]
],
totalElements: 2,
totalPages: 1,
currentPage: 0
)
}
}Spring Cloud Contract 不僅支援 HTTP 合約,也支援訊息驅動(Messaging)合約。
Contract.make {
description "當使用者建立時,應該發送 USER_CREATED 事件"
// 標籤:用來觸發訊息的標記
label "user_created"
// 輸入:觸發訊息發送的條件
input {
triggeredBy("publishUserCreatedEvent()")
}
// 輸出訊息
outputMessage {
sentTo "user-events"
headers {
header("contentType", applicationJson())
}
body(
eventType: "USER_CREATED",
userId: $(producer(regex(positiveInt())), consumer(1)),
userName: $(producer(regex(nonEmpty())), consumer("Alice")),
email: $(producer(regex(email())), consumer("alice@example.com")),
timestamp: $(producer(regex(isoDateTime())), consumer("2024-01-01T00:00:00Z"))
)
}
}@SpringBootTest
public abstract class MessagingBaseContractTest {
@Autowired
private UserEventPublisher userEventPublisher;
// 方法名稱必須與合約中的 triggeredBy 一致
public void publishUserCreatedEvent() {
userEventPublisher.publishUserCreated(1L, "Alice", "alice@example.com");
}
}@SpringBootTest
@AutoConfigureStubRunner(
ids = "com.example:messaging-producer:+:stubs",
stubsMode = StubRunnerProperties.StubsMode.LOCAL
)
public class UserEventConsumerTest {
@Autowired
private StubTrigger stubTrigger;
@Test
void shouldReceiveUserCreatedEvent() {
// 觸發訊息
stubTrigger.trigger("user_created");
// 驗證 Consumer 正確處理了訊息
// ...
}
}原因: baseClassForTests 設定不正確。
解決: 確認 pom.xml 中的完整類別名稱正確:
<baseClassForTests>
com.example.producer.BaseContractTest
</baseClassForTests>原因: Producer 的 Stub JAR 尚未安裝到本地 Maven 倉庫。
解決: 在 Producer 端先執行:
cd producer
mvn clean install原因: 合約中的 consumer() 值與測試預期不一致。
解決: 檢查合約中的 consumer() 部分是否正確:
// consumer() 的值會成為 Stub 回傳的值
id: $(producer(regex(positiveInt())), consumer(42))
// Consumer 測試中應該預期 id = 42解決: 使用 priority 設定優先順序,或確保 request 的區分度(不同 header、body 等)。
方案一:本地安裝
# 先 build Producer
cd producer && mvn clean install
# 再 test Consumer
cd ../consumer && mvn clean test方案二:使用遠端倉庫
@AutoConfigureStubRunner(
ids = "com.example:contract-producer:+:stubs:8080",
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "https://nexus.example.com/repository/maven-releases/"
)方案三:使用 Git 倉庫
@AutoConfigureStubRunner(
ids = "com.example:contract-producer",
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "git://https://github.com/example/contracts.git"
)- 查看自動產生的測試:
target/generated-test-sources/contracts/ - 查看產生的 Stub:
target/stubs/ - 開啟 WireMock 日誌:
# application-test.yml
logging:
level:
com.github.tomakehurst.wiremock: DEBUG| 實踐 | 說明 |
|---|---|
| 合約放在 Producer 端 | Producer 維護合約,確保合約與實作一致 |
| 按功能分目錄 | 例如 contracts/user/、contracts/order/ |
| 合約命名清晰 | 使用 should... 開頭描述預期行為 |
| 版本控制 | 合約隨程式碼一起版本控制 |
-
一個合約只測試一個情境
- 好:
shouldReturnUserById.groovy - 壞:把所有 User 相關測試塞在同一個合約
- 好:
-
使用正則表達式增加彈性
// 好:允許 ID 是任何正整數 id: $(producer(regex(positiveInt())), consumer(1)) // 壞:硬編碼 ID id: 1
-
只驗證 Consumer 真正需要的欄位
- 不要過度約束 Producer 的回應格式
-
涵蓋成功與失敗場景
- 別忘了 404、400、500 等錯誤情境
1. Consumer 團隊定義需求(希望 API 長怎樣)
│
▼
2. 與 Producer 團隊討論,達成共識
│
▼
3. 撰寫合約(可以由任一方撰寫)
│
▼
4. Producer 團隊實作 API,通過合約測試
│
▼
5. 安裝 Stub JAR 到倉庫
│
▼
6. Consumer 團隊使用 Stub 進行測試
git clone <repository-url>
cd spring-cloud-contract-tutorialcd producer
mvn clean install成功後會看到:
[INFO] Installing .../contract-producer-0.0.1-SNAPSHOT.jar
[INFO] Installing .../contract-producer-0.0.1-SNAPSHOT-stubs.jar
cd ../consumer
mvn clean test嘗試修改 shouldReturnUserById.groovy 中的回應格式,然後重新執行 Producer 測試:
cd ../producer
mvn clean test你會看到合約測試失敗,因為 API 回應不再符合合約。
修改 UserController 或 UserService 使 API 回應符合新合約,然後重新執行:
mvn clean install # 重新產生 Stub| 工具 | 版本 |
|---|---|
| Java | 17+ |
| Maven | 3.8+ |
| Spring Boot | 3.2.x |
| Spring Cloud | 2023.0.x |
| Spring Cloud Contract | 4.1.x |
本教程以 MIT 授權條款釋出,歡迎自由使用與修改。