Skip to content

contract_en.md

maoxiaoyue edited this page May 14, 2026 · 1 revision

pkg/contract -- Built-in Contract Testing

Automatically validates handler behavior based on schema-first route metadata, ensuring AI-generated code conforms to declared contracts.

Design Philosophy

When AI generates a handler, how do you know it's correct? Contract Testing makes "schema as contract" -- both request and response body structures must conform to the Input/Output types declared in Schema():

Schema declares Input:  CreateUserRequest{Name, Email}
Schema declares Output: UserResponse{ID, Name, Email}
    |
Request  {"name":"alice"}                 <- missing email -> Input validation fails
Response {"id":1,"name":"alice"}          <- missing email -> Output validation fails

Bidirectional Schema Validation

Contract Testing validates both Input (request body) and Output (response body):

  • Input validation: When ExpectSchema: true and the Schema defines an Input type, it automatically validates whether tc.Input JSON conforms to the Input struct
  • Output validation: Validates whether the handler response JSON conforms to the Output struct (including required field checks)

Quick Start

Manual Testing

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

func TestCreateUser(t *testing.T) {
    r := setupRouter()  // Router with Schema routes registered

    contract.Test(t, r, contract.TestCase{
        Route:        "POST /api/users",
        Input:        `{"name":"alice","email":"alice@test.com"}`,
        ExpectStatus: 201,
        ExpectSchema: true,  // Validate response conforms to Output schema
    })
}

Auto-test All Routes

Test all schema-registered routes with a single line of code:

func TestAllRoutes(t *testing.T) {
    r := setupRouter()
    contract.TestAll(t, r)  // Automatically generates tests for each schema route
}

TestAll will:

  1. Get all registered schema routes from schema.Global()
  2. Auto-generate a minimal valid request body for each route
  3. Auto-resolve path parameters (:id -> 1)
  4. Validate status codes and response schema

Simple Route Existence Testing

No schema needed -- just verify the route exists and returns the correct status code:

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

TestCase Fields

type TestCase struct {
    Route        string            // "METHOD /path", e.g. "POST /api/users"
    Input        string            // JSON request body
    Headers      map[string]string // Custom request headers
    Query        map[string]string // URL query parameters
    ExpectStatus int               // Expected HTTP status code
    ExpectSchema bool              // Whether to validate response against Output schema
    ExpectBody   string            // Exact match response body (optional)
}

Validation Mechanisms

Schema Validation (ExpectSchema: true)

When ExpectSchema is true, contract will:

  1. Look up the route's schema from schema.Global()
  2. Get the Output type (Go struct)
  3. Attempt to deserialize the response body JSON into that struct
  4. Check that all required fields are present
// Schema declaration
r.Schema(schema.Route{
    Method: "GET",
    Path:   "/api/users/:id",
    Output: UserResponse{},  // Has 3 required fields: ID, Name, Email
}).Handle(getUserHandler)

// If handler returns {"id":1} -> fails (missing name, email)
// If handler returns {"id":1,"name":"a","email":"b"} -> passes

Required Field Rules

struct tag Required?
json:"name" Yes
json:"bio,omitempty" No
Bio *string \json:"bio"`` No (pointer)

Status Code Validation

If ExpectStatus > 0, validates that the response status code matches.

Exact Body Match

If ExpectBody is not empty, trims whitespace and performs an exact match against the response body.

Multi-Protocol Support

TestAll automatically distinguishes protocols:

Protocol Behavior
REST (Protocol empty or "rest") Sends HTTP request, validates Input/Output and status code
gRPC / Bot / MCP / WebSocket / CLI Validates schema definition completeness (Command non-empty, Summary non-empty, type names populated)
// TestAll with mixed protocols
schema.RegisterGRPC("UserService/CreateUser", "Create user", req, resp)
schema.RegisterBot("/start", "Start the bot", nil, WelcomeMsg{})

contract.TestAll(t, router)
// → REST routes: full HTTP testing
// → gRPC routes: schema completeness validation
// → Bot routes: schema completeness validation

Automatic Test Generation (TestAll — REST)

For REST routes, TestAll uses the following strategies to auto-generate test cases:

Path Parameter Resolution

Path Resolved Result
/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

Status Code Inference

Condition Inferred Status Code
Responses has explicit 2xx declaration Uses the smallest 2xx
POST (no declaration) 201
DELETE (no declaration) 204
Others (no declaration) 200

Request Body Generation

Body is auto-generated only for POST, PUT, PATCH. Fills reasonable default values based on the Input struct fields:

Go Type Generated Value
string "test"
int, int64 0
float64 0.0
bool false
[]T []
map[K]V {}

Complete Example

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: "Create user",
        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
}

// Manual test for a specific route
func TestCreateUser(t *testing.T) {
    r := setupRouter()
    contract.Test(t, r, contract.TestCase{
        Route:        "POST /api/users",
        Input:        `{"name":"alice","email":"alice@test.com"}`,
        ExpectStatus: 201,
        ExpectSchema: true,
    })
}

// Test with query parameters
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,
    })
}

// Test with custom headers
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,
    })
}

// One-click test for all schema routes
func TestAllContracts(t *testing.T) {
    r := setupRouter()
    contract.TestAll(t, r)
}

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

Architecture

pkg/contract/
├── contract.go      Test(), TestAll(), TestRoute() core test functions
├── validate.go      validateResponse(), validateRequest(), validateRequiredFields()
├── generate.go      generateTestCase(), generateMinimalJSON(), resolvePath()
└── contract_test.go 24 unit tests

Dependencies

pkg/contract -> pkg/router (Router.ServeHTTP to execute requests)
             -> pkg/schema (Global() to look up schema metadata)
             -> net/http/httptest (simulate HTTP requests)

Relationship with Schema

Schema-first Routes          Contract Testing
+------------------+      +------------------+
| Route{           |      | TestCase{        |
|   Input:  Req{}  |----->|   ExpectSchema:  |
|   Output: Resp{} |      |     true         |
|   Responses: ... |      | }                |
| }                |      |                  |
+------------------+      +------------------+
         |                         |
         +--- schema.Global() ----+
              (shared Registry)

Routes not registered with Schema() cannot undergo schema validation, but can still use TestRoute() to check status codes.

HypGo

繁體中文 | English


中文文件

設計文件

套件

AI 協作工具鏈

CLI 命令


English Docs

Design Docs

Packages

AI Collaboration Toolchain

CLI Commands

Clone this wiki locally