概念定位

Contract testing 在服務邊界上驗證 producer 與 consumer 的相容性,把跨團隊協作的隱性期待變成可執行的契約

這一頁處理的是服務邊界上的信任問題。當服務彼此頻繁演進,契約測試是避免變更互相踩踏的最小保護層。契約對準的是真實 consumer 的期待,而不是抽象的 spec 文件。

核心判讀

好的 contract testing 會明確劃出兼容視窗,並把驗證放進 CI 或 release gate

判讀時看三件事:

  • 契約是否對準真實 consumer,而非假想 client
  • schema evolution 是否有明確 compatibility window
  • 失敗是否能回到責任邊界,而非只看到測試紅燈

Consumer-driven vs Provider-driven

契約驗證有兩個驅動方向,適用場景不同。

Consumer-driven:consumer 先定義對 producer 回應的期望(欄位、型別、值域),producer 驗證是否能滿足。這種做法讓驗證對準真實消費需求 — consumer 只關心它用到的欄位,producer 可以自由演進不被使用的部分。缺點是 consumer 數量多時,契約管理成本上升:每個 consumer 維護自己的契約檔,producer 需要跑所有 consumer 契約才能確認相容。

Provider-driven:producer 定義 API spec(OpenAPI / gRPC schema),consumer 驗證自己能否適配。producer 主導 schema 演進節奏,consumer 接收變更通知並更新。這種做法適合公開 API 或 consumer 數量大且不可控的服務。缺點是可能漏掉 consumer 依賴的隱性行為 — spec 上合規但語意變了,consumer 仍會失敗。

判斷依據:consumer 少且已知(內部微服務)→ consumer-driven;consumer 多或不可控(公開 API / 平台整合)→ provider-driven。兩者可混用:核心 consumer 用 consumer-driven 保護關鍵路徑,其他 consumer 靠 provider spec 覆蓋。

契約驗證的三個層次

契約驗證按深度分三層,每一層攔截不同類型的破壞。

層次驗證內容常見工具
Schema 結構欄位是否存在、型別是否一致JSON Schema validation / protobuf 編譯
語意相容值域、enum 範圍、nullable 語意是否對齊Pact interaction / custom assertion
向後相容性新版輸出能否被舊版 consumer 解析Avro compatibility check / Buf

Schema 結構是最基礎的防線。欄位缺失或型別錯誤會直接導致 runtime 解析失敗。這一層成本低、回饋快,適合放在 CI fast path。

語意相容攔截的是「schema 通過但行為不同」的問題。例如某個欄位從 nullable 改成 required,或 enum 新增一個值但 consumer 的 switch 沒有 default branch。這類問題在結構層驗證不出來,需要 consumer 定義語意期望(Pact interaction 的 matcher / assertion)。

向後相容性是跨版本共存的保障。Avro 和 Protobuf 有內建 compatibility mode(backward / forward / full);JSON Schema 需要外部工具(如 json-schema-diff)做版本比較。向後相容性驗證的成本最高,但能攔截最嚴重的破壞 — 一旦 event 寫入 broker,舊版 consumer 就必須能解析它。

Schema 演進規則

Schema 演進按協議類型有不同的安全邊界。

API schema(OpenAPI / gRPC)

API schema 的演進判讀:新增可選欄位通常安全;移除欄位、重新命名欄位、或把可選改成必填是 breaking change;型別變更(如 int32 → int64)視 consumer 的容忍度而定。gRPC 的 field number 機制讓欄位新增與移除的相容性比 JSON 更明確 — 未知 field number 被忽略,已知 field number 被刪除會觸發 default value,兩者都有可預測行為。

Event schema(Avro / Protobuf / JSON Schema)

Event schema 的相容性要求比 API 更嚴格。API 的 breaking change 可以靠 versioning(/v2/)隔離,event 一旦寫入 broker 就跟所有版本的 consumer 共存。backward compatibility(新 schema 能讀舊資料)是最低要求;forward compatibility(舊 schema 能讀新資料)讓 consumer 可以延遲升級。

Schema registry(Confluent Schema Registry / AWS Glue Schema Registry)提供集中式的相容性 gate:producer 註冊新版 schema 前,registry 自動比對相容性規則,拒絕 breaking change。這個 gate 比 CI 更早攔截,因為它在 schema 發布時就生效。

DB schema 演進的契約驗證銜接到 6.11 migration safety — expand/contract pattern 讓新舊版本共存,本質上跟 event schema 的 backward compatibility 是同一個問題。

CI 整合

Contract test 在 CI 的位置跟 unit test 不同 — 需要跨服務的契約同步。

Fast path:producer 的 schema 變更觸發 consumer 的 contract test。實作上需要 CI 能跨 repo 觸發(webhook / pipeline trigger),或用 contract broker(如 Pact Broker)做非同步驗證。fast path 只跑受影響 consumer 的契約,保持回饋速度。

Slow path:完整 contract matrix 驗證 — 所有 consumer × producer 組合。這個矩陣在 merge gate 或 scheduled path 跑,覆蓋 fast path 漏掉的間接影響。矩陣規模隨服務數增長,需要 selective matrix(只跑有變更的 producer 相關 consumer)控制成本。

失敗處理:contract test 失敗時的責任分派是關鍵流程。失敗可能來自 producer 的 breaking change,也可能來自 consumer 的 expectation 過期。Pact 的 can-i-deploy 機制提供自動化判斷:比對 producer 當前版本與 consumer 上次驗證通過的版本,定位責任方。

案例對照

  • Stripe:外部整合的 API 需要嚴格的 backward compatibility — 交易 API 的 breaking change 會直接影響商戶收入,schema 演進靠 expand/contract 逐步過渡。
  • Shopify:跨服務 deploy 順序錯誤是高峰期常見事故源 — contract test 攔截 schema 不相容,讓 deploy 順序有驗證依據。
  • GitHub:API 與 webhook 的契約覆蓋面廣,契約失配會直接影響整合生態。

判讀訊號

訊號判讀條件行動建議
跨服務 deploy 順序錯誤導致 production 故障contract test 應在 CI 攔截相容性問題,deploy 順序才有驗證依據補 contract test 到 CI fast path
API 文件跟實作漂移、新接入服務出意外provider-driven spec 需要自動化 diff 偵測,手動更新會漂移接 OpenAPI diff 工具到 CI、spec 變更自動 PR
event schema 變更後下游 consumer 解析失敗schema registry 的 compatibility gate 應在 publish 前攔截啟用 schema registry 的 compatibility check
breaking change 靠 release note 標註標註是通知、contract test 是攔截,兩者責任不同加 CI contract gate 攔截 breaking change
contract 違規只在 staging 才發現contract test 應在 CI fast path 跑,staging 發現代表 CI 沒覆蓋把 contract test 從 staging 提前到 CI push 觸發

交接路由