
Architecture Journal
契約測試:AI 時代自動化測試與驗收(六)三種方案:OpenAPI + Schemathesis、Pact 與 Spring Cloud Contract
這篇接著前一篇談契約測試的定位,整理 OpenAPI + Schemathesis、Pact 與 Spring Cloud Contract 各自驗證什麼、怎麼運作,以及如何放進 CI。
API、微服務或訊息系統之間最麻煩的問題,通常不是某個函式算錯,而是系統邊界的約定慢慢漂移了。
文件說 response 一定有 id,實際卻回傳 null;consumer 期待 404,provider 改成了 200;某個欄位被重新命名,直到前端或另一個服務部署後才發現。
這些問題都可以歸結成一件事:
實際系統是否仍然遵守對外公布的 contract?
契約測試(contract testing)就是用自動化測試,驗證系統之間對 request、response、message 或事件格式的共同約定沒有被破壞。
不過,契約測試不是一個單一工具,也不是只有一種工作流。OpenAPI + Schemathesis、Pact 和 Spring Cloud Contract 都能用來驗證契約,但它們解決的問題不完全一樣。
三組方案其實在驗證不同問題
可以先用下面這張圖分辨工具的責任:
OpenAPI + Schemathesis
→ 這個 endpoint 的回應是否符合 OpenAPI schema?
Pact
→ 這個 provider 是否滿足特定 consumer 的實際需求?
Spring Cloud Contract
→ 能不能從 contract 產生 provider tests 與 consumer stubs?
OpenAPI + Schemathesis 偏向 schema-driven、黑箱式的 endpoint testing。它從 OpenAPI 產生輸入,呼叫實際 endpoint,再驗證 response。
Pact 偏向 consumer-driven contract testing。consumer 先描述自己真正依賴的 request 和 response,provider 再驗證是否能滿足這些 expectation。
Spring Cloud Contract 則把 contract DSL、provider-side generated tests、WireMock stubs 和 artifact repository 串成一套 workflow。它既支援 provider-side contract,也支援 consumer-driven contract;實際差異在於 contract 的所有權與發布流程。
OpenAPI + Schemathesis:最接近「給 contract 和 endpoint,直接產出報告」
如果需求是:
給定一份 API contract 和一個 endpoint,自動呼叫 API、驗證結果,再產出 CI 可以讀的報告。
那最直接的選擇通常是 OpenAPI + Schemathesis。
OpenAPI 可以描述:
- path、HTTP method、query、header 和 request body
- request 與 response 的欄位、型別和必填條件
- status code
- enum、格式與限制
- 不同狀態下的 response
例如:
openapi: 3.0.3
info:
title: User API
version: 1.0.0
paths:
/users/{user_id}:
get:
parameters:
- name: user_id
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: User found
content:
application/json:
schema:
$ref: "#/components/schemas/User"
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: string
name:
type: string
Schemathesis 會讀取這份 schema,產生多組測試輸入,呼叫實際 API,並檢查 status code、response schema 與 server error。它也能產生邊界值和錯誤輸入,不只測一條手寫的 happy path;但它主要驗證 API 是否符合 schema,不會取代業務規則測試或 E2E 測試。
基本使用方式如下:
schemathesis run openapi.yaml \
--url "$API_BASE_URL" \
--report junit \
--report-dir ./test-reports
Schemathesis 支援 JUnit、HAR、VCR 和 NDJSON 等輸出格式,JUnit XML 可以交給 CI 系統統一收集。Schemathesis 官方文件
GitHub Actions 的概念範例如下:
name: API contract test
on:
pull_request:
push:
jobs:
contract-test:
runs-on: ubuntu-latest
env:
API_BASE_URL: http://127.0.0.1:8000
steps:
- uses: actions/checkout@v4
# 實際專案需要在這裡啟動測試環境
# 例如:docker compose up -d api
- name: Install Schemathesis
run: pip install schemathesis
- name: Run API contract tests
run: |
schemathesis run openapi.yaml \
--url "$API_BASE_URL" \
--report junit \
--report-dir ./test-reports
- name: Upload JUnit report
if: always()
uses: actions/upload-artifact@v4
with:
name: api-contract-test-report
path: test-reports/
這種做法的優點是簡單、跨語言,而且很接近「規格檔對實際服務做驗證」。但它有一個前提:OpenAPI 必須描述得夠準確。如果 schema 本身就是錯的,測試只會很有信心地驗證錯誤規格。
Pact:從 consumer 真正依賴的互動出發
Pact 的核心不是測試完整 API,而是驗證 consumer 和 provider 之間的實際互動。
典型流程如下:
Consumer 撰寫測試
↓
Pact 產生 JSON contract
↓
Consumer CI 發布 contract
↓
Provider CI 取回 contract
↓
Provider replay request 並驗證 response
↓
(可選但推薦)Pact Broker 分享 contract、驗證結果與版本相容性
假設前端只需要 User API 的 id 和 name,另一個服務需要 id 和 email。兩個 consumer 可以各自建立自己的 Pact。Pact contract 不會描述 Provider 整份 API,只記錄特定 consumer 真正依賴的互動;Provider 仍需自行測試其他功能與 consumer。
這是 Pact 很重要的特性:contract 通常從 consumer 的測試互動產生,而不是先描述 provider 的整份 API。Pact 會把這些互動保存成 contract,再讓 provider 端重新執行並驗證。Pact 官方文件
Pact 適合:
- consumer 和 provider 由不同團隊維護
- 多個服務各自獨立部署
- 系統使用多種語言或框架
- 需要知道哪些 consumer/provider 版本可以安全一起部署
- HTTP API 與非同步訊息都需要驗證
Pact Broker 是可選但很實用的共享元件,負責分享 Pact 和 provider verification 結果,也能提供不同版本之間的相容性矩陣。Pact Broker 官方文件
Pact 不適合被當成 provider 的完整功能測試工具。Pact 官方也建議,contract test 應該聚焦在 consumer 的需求與雙方對互動的共同理解,而不是取代 provider 自己的 functional tests。Pact consumer testing
Spring Cloud Contract:把 contract、generated tests 和 stubs 串起來
Spring Cloud Contract 的核心價值,是把一份 contract DSL 或 YAML 轉換成多種測試產物。
常見流程如下:
撰寫 contract DSL/YAML
↓
Provider 產生並執行 contract tests
↓
產生 WireMock stubs
↓
發布至 Maven 相容的 artifact repository,例如 Nexus 或 Artifactory,也可以使用 Git 儲存
↓
Consumer 使用 Stub Runner 啟動 stubs
Provider 端的 contract 可能描述:
request:
method: GET
url: /users/123
response:
status: 200
headers:
Content-Type: application/json
body:
id: "123"
name: Samson
Spring Cloud Contract 可以根據這份定義:
- 產生 provider-side tests
- 驗證實際 provider 的 response
- 產生 WireMock stubs
- 讓 consumer 在測試時使用 provider stubs
- 把 stubs 發布到 artifact repository
Stub Runner 會下載並啟動 provider stubs,讓 consumer 的 integration tests 不必啟動完整的 provider 服務。Spring Cloud Contract Stub Runner
Spring Cloud Contract 最適合 Spring Boot、Java 或 Kotlin 團隊,尤其是已經使用 Maven/Gradle,以及 Nexus/Artifactory 的組織。它也支援非 JVM 應用,但通常需要透過 Docker 或外部 artifact 流程整合,因此跨語言使用時的體驗不如 Pact 直接。Spring Cloud Contract 非 JVM 流程
Pact 和 Spring Cloud Contract 的差異
兩者可以用下面這張表快速比較:
| 面向 | Pact | Spring Cloud Contract |
|---|---|---|
| 核心定位 | Consumer-driven contract testing | Provider/Consumer-driven contract testing |
| 合約來源 | Consumer 測試產生 Pact | DSL 或 YAML contract |
| Provider 驗證 | Replay consumer 的 request | 執行 generated provider tests |
| Consumer 測試 | Pact mock provider | Stub Runner、WireMock 或 message stub |
| 合約交換 | Pact Broker | Maven 相容 repository(例如 Nexus/Artifactory)或 Git |
| 主要生態 | 跨語言、跨框架 | Spring/JVM 整合最完整 |
| 典型重點 | consumer 實際依賴什麼 | contract 如何產生 tests 與 stubs |
更直白地說:
- Pact 比較像「Consumer 說我需要這些互動,Provider 證明自己做得到」。
- Spring Cloud Contract 比較像「團隊維護一份 contract,從它產生 provider tests 和 consumer stubs」。
兩者都能做 consumer-driven contract testing,所以差異不是「一個能做、另一個不能做」,而是預設 workflow、工具鏈和生態系不同。
實務上可以並存
這些工具不一定要互相取代。它們可以分層使用:
OpenAPI + Schemathesis
→ 驗證 OpenAPI 描述的 API response 是否符合 schema
Pact/Spring Cloud Contract
→ 驗證重要 consumer-provider 互動
Unit/integration tests
→ 驗證業務邏輯與基礎設施整合
E2E tests
→ 驗證少量關鍵使用者旅程
例如一個服務可以同時有:
- Schemathesis:確保 OpenAPI 與實際 endpoint 沒有 drift
- Pact:確保前端使用的幾個重要 User API interaction 不會被 provider 破壞
- Unit tests:驗證權限、狀態轉換與錯誤處理
- E2E:驗證使用者能完成登入、下單或付款流程
這樣的分工比用單一工具包辦所有測試更清楚。每個工具只需要回答一個具體問題。
OpenAPI 定義 API 應該長什麼樣,Schemathesis 驗證 API 實際長什麼樣;Pact 驗證 consumer 依賴的互動,Spring Cloud Contract 則把 contract、provider tests 和 consumer stubs 串成一套工程流程。