Skip to main content
契約測試:AI 時代自動化測試與驗收(六)三種方案:OpenAPI + Schemathesis、Pact 與 Spring Cloud Contract

Architecture Journal

TestingContractTestingAPITesting

契約測試: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 的 idname,另一個服務需要 idemail。兩個 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 的差異

兩者可以用下面這張表快速比較:

面向PactSpring Cloud Contract
核心定位Consumer-driven contract testingProvider/Consumer-driven contract testing
合約來源Consumer 測試產生 PactDSL 或 YAML contract
Provider 驗證Replay consumer 的 request執行 generated provider tests
Consumer 測試Pact mock providerStub Runner、WireMock 或 message stub
合約交換Pact BrokerMaven 相容 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 串成一套工程流程。