contract.md - maoxiaoyue/hypgo GitHub Wiki

pkg/contract — Contract Testing 內建驗證

根據 schema-first 路由的 metadata 自動驗證 handler 行為,確保 AI 生成的程式碼符合宣告的合約。

設計理念

AI 生成了一個 handler,怎麼知道它是對的?Contract Testing 讓「schema 即合約」——請求和回應的 body 結構都必須符合 Schema() 宣告的 Input/Output 型別:

Schema 宣告 Input:  CreateUserRequest{Name, Email}
Schema 宣告 Output: UserResponse{ID, Name, Email}
    ↓
Request  {"name":"alice"}                 ← 缺 email → Input 驗證失敗  ✅
Response {"id":1,"name":"alice"}          ← 缺 email → Output 驗證失敗 ✅

雙向 Schema 驗證

Contract Testing 同時驗證 Input(請求 body)和 Output(回應 body):

  • Input 驗證:當 ExpectSchema: true 且 Schema 定義了 Input 型別時,自動驗證 tc.Input JSON 是否符合 Input struct
  • Output 驗證:驗證 handler 回應的 JSON 是否符合 Output struct(含必填欄位檢查)

快速上手

手動測試

import (
    "testing"
    "github.com/maoxiaoyue/hypgo/pkg/contract"
)

func TestCreateUser(t *testing.T) {
    r := setupRouter()  // 已註冊 Schema 路由的 router

    contract.Test(t, r, contract.TestCase{
        Route:        "POST /api/users",
        Input:        `{"name":"alice","email":"[email protected]"}`,
        ExpectStatus: 201,
        ExpectSchema: true,  // 驗證 response 符合 Output schema
    })
}

自動測試所有路由

一行程式碼測試所有 schema-registered 路由:

func TestAllRoutes(t *testing.T) {
    r := setupRouter()
    contract.TestAll(t, r)  // 自動為每個 schema 路由生成測試
}

TestAll 會:

  1. schema.Global() 取得所有已註冊的 schema 路由
  2. 為每個路由自動生成最小有效的請求 body
  3. 自動解析路徑參數(:id1
  4. 驗證狀態碼和回應 schema

簡易路由存在性測試

不需要 schema,只驗證路由是否存在且回傳正確狀態碼:

func TestHealthEndpoint(t *testing.T) {
    r := setupRouter()
    contract.TestRoute(t, r, "GET", "/health", 200)
}

TestCase 欄位

type TestCase struct {
    Route        string            // "METHOD /path",例如 "POST /api/users"
    Input        string            // JSON 請求 body
    Headers      map[string]string // 自訂請求標頭
    Query        map[string]string // URL query 參數
    ExpectStatus int               // 期望的 HTTP 狀態碼
    ExpectSchema bool              // 是否驗證回應符合 Output schema
    ExpectBody   string            // 精確比對回應 body(可選)
}

驗證機制

Schema 驗證 (ExpectSchema: true)

ExpectSchema 為 true 時,contract 會:

  1. schema.Global() 查找該路由的 schema
  2. 取得 Output 型別(Go struct)
  3. 嘗試將回應 body JSON 反序列化為該 struct
  4. 檢查所有必填欄位是否存在
// schema 宣告
r.Schema(schema.Route{
    Method: "GET",
    Path:   "/api/users/:id",
    Output: UserResponse{},  // 有 ID, Name, Email 三個必填欄位
}).Handle(getUserHandler)

// 若 handler 回傳 {"id":1} → 失敗(缺 name, email)
// 若 handler 回傳 {"id":1,"name":"a","email":"b"} → 通過

必填欄位判定

struct tag Required?
json:"name" ✅ 是
json:"bio,omitempty" ❌ 否
Bio *string \json:"bio"`` ❌ 否(pointer)

狀態碼驗證

ExpectStatus > 0,驗證回應狀態碼是否匹配。

Body 精確比對

ExpectBody 非空,去除首尾空白後精確比對回應 body。

自動測試生成(TestAll)

TestAll 內部使用以下策略自動生成測試案例:

路徑參數解析

路徑 解析結果
/api/users/:id /api/users/1
/api/users/:userId/posts/:postId /api/users/1/posts/1
/api/:slug /api/test-slug
/api/:name /api/test
/files/*filepath /files/test.txt

狀態碼推測

條件 推測的狀態碼
Responses 中有明確宣告的 2xx 使用最小的 2xx
POST(無宣告) 201
DELETE(無宣告) 204
其他(無宣告) 200

請求 Body 生成

僅對 POST、PUT、PATCH 自動生成 body。根據 Input struct 的欄位,填入合理預設值:

Go 型別 生成的值
string "test"
int, int64 0
float64 0.0
bool false
[]T []
map[K]V {}

完整範例

package api_test

import (
    "testing"
    "github.com/maoxiaoyue/hypgo/pkg/contract"
    "github.com/maoxiaoyue/hypgo/pkg/router"
    "github.com/maoxiaoyue/hypgo/pkg/schema"
)

type CreateUserReq struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

type UserResp struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func setupRouter() *router.Router {
    r := router.New()

    r.Schema(schema.Route{
        Method:  "POST",
        Path:    "/api/users",
        Summary: "建立使用者",
        Input:   CreateUserReq{},
        Output:  UserResp{},
        Responses: map[int]schema.ResponseSchema{
            201: {Description: "Created"},
        },
    }).Handle(createUserHandler)

    r.Schema(schema.Route{
        Method: "GET",
        Path:   "/api/users/:id",
        Output: UserResp{},
    }).Handle(getUserHandler)

    r.GET("/health", healthHandler)

    return r
}

// 手動測試特定路由
func TestCreateUser(t *testing.T) {
    r := setupRouter()
    contract.Test(t, r, contract.TestCase{
        Route:        "POST /api/users",
        Input:        `{"name":"alice","email":"[email protected]"}`,
        ExpectStatus: 201,
        ExpectSchema: true,
    })
}

// 帶 query 參數的測試
func TestSearchUsers(t *testing.T) {
    r := setupRouter()
    contract.Test(t, r, contract.TestCase{
        Route:        "GET /api/users",
        Query:        map[string]string{"page": "1", "limit": "10"},
        ExpectStatus: 200,
    })
}

// 帶自訂標頭的測試
func TestAuthRequired(t *testing.T) {
    r := setupRouter()
    contract.Test(t, r, contract.TestCase{
        Route:        "GET /api/admin",
        Headers:      map[string]string{"Authorization": "Bearer test-token"},
        ExpectStatus: 200,
    })
}

// 一鍵測試所有 schema 路由
func TestAllContracts(t *testing.T) {
    r := setupRouter()
    contract.TestAll(t, r)
}

// 簡易路由檢查
func TestHealth(t *testing.T) {
    r := setupRouter()
    contract.TestRoute(t, r, "GET", "/health", 200)
}

架構

pkg/contract/
├── contract.go      Test()、TestAll()、TestRoute() 核心測試函式
├── validate.go      validateResponse()、validateRequest()、validateRequiredFields()
├── generate.go      generateTestCase()、generateMinimalJSON()、resolvePath()
└── contract_test.go 24 個單元測試

依賴關係

pkg/contract → pkg/router(Router.ServeHTTP 執行請求)
             → pkg/schema(Global() 查找 schema metadata)
             → net/http/httptest(模擬 HTTP 請求)

與 Schema 的關係

Schema-first 路由          Contract Testing
┌──────────────────┐      ┌──────────────────┐
│ Route{           │      │ TestCase{        │
│   Input:  Req{}  │─────→│   ExpectSchema:  │
│   Output: Resp{} │      │     true         │
│   Responses: ... │      │ }                │
│ }                │      │                  │
└──────────────────┘      └──────────────────┘
         │                         │
         └─── schema.Global() ────┘
              (共用 Registry)

不使用 Schema() 註冊的路由無法進行 schema 驗證,但仍可使用 TestRoute() 檢查狀態碼。

測試

go test ./pkg/contract/... -v

涵蓋:基本測試、schema 驗證、body 比對、query 參數、自訂標頭、404、TestAll 自動測試、TestRoute、validateResponse(正向/缺欄位/空 body/nil type/optional)、validateRequest、generateTestCase、路徑解析、狀態碼推測。