スキル一覧に戻る
janisto

go-testing

by janisto

Huma playground

0🍴 0📅 2026年1月25日
GitHubで見るManusで実行

SKILL.md


name: go-testing description: Guide for writing Go tests following this project's patterns including httptest, test organization, and coverage requirements.

Go Testing

Use this skill when writing tests for this Huma REST API application.

For comprehensive testing guidelines, see AGENTS.md in the repository root.

Test Organization

Tests are colocated with source files using _test.go suffix:

internal/
    http/
        v1/
            routes/
                routes.go
                routes_test.go
            items/
                handler.go
                handler_test.go
    platform/
        logging/
            middleware.go
            middleware_test.go

Test Server Setup

Create test servers using Chi router and Huma:

package routes_test

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/danielgtaylor/huma/v2"
    humachi "github.com/danielgtaylor/huma/v2/adapters/humachi"
    "github.com/go-chi/chi/v5"
    chimiddleware "github.com/go-chi/chi/v5/middleware"

    "github.com/janisto/huma-playground/internal/http/health"
    "github.com/janisto/huma-playground/internal/http/v1/routes"
    applog "github.com/janisto/huma-playground/internal/platform/logging"
    appmiddleware "github.com/janisto/huma-playground/internal/platform/middleware"
    "github.com/janisto/huma-playground/internal/platform/respond"
)

func setupTestRouter() *chi.Mux {
    router := chi.NewRouter()
    router.Use(
        appmiddleware.RequestID(),
        chimiddleware.RealIP,
        applog.RequestLogger(),
        respond.Recoverer(),
    )

    // Root-level endpoints (unversioned)
    router.Get("/health", health.Handler)

    // Versioned API
    router.Route("/v1", func(r chi.Router) {
        api := humachi.New(r, huma.DefaultConfig("Test", "test"))
        routes.Register(api)
    })
    return router
}

Basic Test Pattern

func TestHealthEndpoint(t *testing.T) {
    router := setupTestRouter()

    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    req.Header.Set(chimiddleware.RequestIDHeader, "test-trace-id")
    resp := httptest.NewRecorder()

    router.ServeHTTP(resp, req)

    if resp.Code != http.StatusOK {
        t.Fatalf("expected 200, got %d", resp.Code)
    }

    var body health.Response
    if err := json.Unmarshal(resp.Body.Bytes(), &body); err != nil {
        t.Fatalf("failed to decode response: %v", err)
    }
    if body.Status != "healthy" {
        t.Fatalf("unexpected status: %s", body.Status)
    }
}

Testing Error Responses

Verify RFC 9457 Problem Details format:

func TestNotFoundReturns404(t *testing.T) {
    router := setupTestRouter()

    req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil)
    resp := httptest.NewRecorder()

    router.ServeHTTP(resp, req)

    if resp.Code != http.StatusNotFound {
        t.Fatalf("expected 404, got %d", resp.Code)
    }

    var problem huma.ErrorModel
    if err := json.Unmarshal(resp.Body.Bytes(), &problem); err != nil {
        t.Fatalf("failed to unmarshal problem: %v", err)
    }
    if problem.Status != http.StatusNotFound {
        t.Fatalf("expected status 404, got %d", problem.Status)
    }
    if problem.Title != "Not Found" {
        t.Fatalf("unexpected title: %s", problem.Title)
    }
}

Testing POST Requests

func TestCreateResource(t *testing.T) {
    router := setupTestRouter()

    body := `{"name": "Test Resource"}`
    req := httptest.NewRequest(http.MethodPost, "/v1/resources", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    resp := httptest.NewRecorder()

    router.ServeHTTP(resp, req)

    if resp.Code != http.StatusCreated {
        t.Fatalf("expected 201, got %d", resp.Code)
    }

    location := resp.Header().Get("Location")
    if location == "" {
        t.Fatal("expected Location header")
    }
}

Testing Validation Errors

func TestValidationReturns422(t *testing.T) {
    router := setupTestRouter()

    body := `{"name": ""}` // Empty name should fail validation
    req := httptest.NewRequest(http.MethodPost, "/v1/resources", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    resp := httptest.NewRecorder()

    router.ServeHTTP(resp, req)

    if resp.Code != http.StatusUnprocessableEntity {
        t.Fatalf("expected 422, got %d", resp.Code)
    }

    var problem huma.ErrorModel
    if err := json.Unmarshal(resp.Body.Bytes(), &problem); err != nil {
        t.Fatalf("failed to unmarshal: %v", err)
    }
    if len(problem.Errors) == 0 {
        t.Fatal("expected validation errors")
    }
}

Table-Driven Tests

Use subtests for comprehensive coverage:

func TestListItems(t *testing.T) {
    router := setupTestRouter()

    tests := []struct {
        name       string
        query      string
        wantStatus int
        wantItems  int
    }{
        {"default limit", "", http.StatusOK, 20},
        {"custom limit", "?limit=5", http.StatusOK, 5},
        {"filter category", "?category=electronics", http.StatusOK, 10},
        {"invalid cursor", "?cursor=invalid", http.StatusBadRequest, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest(http.MethodGet, "/v1/items"+tt.query, nil)
            resp := httptest.NewRecorder()

            router.ServeHTTP(resp, req)

            if resp.Code != tt.wantStatus {
                t.Errorf("got status %d, want %d", resp.Code, tt.wantStatus)
            }
        })
    }
}

For paginated endpoints:

func TestPaginationLinkHeader(t *testing.T) {
    router := setupTestRouter()

    req := httptest.NewRequest(http.MethodGet, "/v1/items?limit=5", nil)
    resp := httptest.NewRecorder()

    router.ServeHTTP(resp, req)

    if resp.Code != http.StatusOK {
        t.Fatalf("expected 200, got %d", resp.Code)
    }

    link := resp.Header().Get("Link")
    if link == "" {
        t.Fatal("expected Link header for pagination")
    }

    if !strings.Contains(link, `rel="next"`) {
        t.Error("expected next link in Link header")
    }
}

Testing Content Negotiation

func TestCBORResponse(t *testing.T) {
    router := setupTestRouter()

    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    req.Header.Set("Accept", "application/cbor")
    resp := httptest.NewRecorder()

    router.ServeHTTP(resp, req)

    if resp.Code != http.StatusOK {
        t.Fatalf("expected 200, got %d", resp.Code)
    }

    contentType := resp.Header().Get("Content-Type")
    if !strings.Contains(contentType, "application/cbor") {
        t.Errorf("expected CBOR content type, got %s", contentType)
    }
}

Test Naming Convention

Pattern: Test<Function>_<Scenario> or Test<Endpoint>Returns<Status><Condition>

func TestHealthEndpoint(t *testing.T) { ... }
func TestCreateResource_Returns201OnSuccess(t *testing.T) { ... }
func TestGetResource_Returns404WhenNotFound(t *testing.T) { ... }
func TestListItems_WithInvalidCursor_Returns400(t *testing.T) { ... }

Running Tests

# Run all tests
go test ./...

# Verbose output
go test -v ./...

# With coverage
go test -v -covermode=atomic -coverpkg=./... -coverprofile=coverage.out ./...

# Coverage report
go tool cover -func=coverage.out
go tool cover -html=coverage.out -o coverage.html

Coverage Requirements

Tests should cover:

  • Success paths (200, 201, 204)
  • Error paths (400, 404, 422, 500)
  • Edge cases (empty input, boundary values)
  • Problem Details format verification
  • Trace ID propagation
  • Content negotiation (JSON/CBOR)

Important Notes

  • Always set X-Request-ID header for trace testing
  • Verify response Content-Type matches Accept header
  • Check Problem Details structure for all error responses
  • Test both valid and invalid enum values
  • Verify Location header for 201 Created responses

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です