Communitygithub.com

Go 測試模式

Go 測試模式,包括表格驅動測試、子測試、基準測試、模糊測試和測試覆蓋率。遵循 TDD 方法論與慣用 Go 實踐。在編寫 Go 測試時使用——表格驅動案例、子測試、基準測試、模糊測試或覆蓋率。

Go 測試模式 是什麼?

這是一套全面的 Go 測試模式指南,旨在幫助開發者遵循 TDD(測試驅動開發)方法論編寫可靠、可維護的測試。它涵蓋了從基礎的表格驅動測試到高級主題如模糊測試和 HTTP 處理器測試的所有內容。該技能支持多個平台,如 Claude Code、Cursor 和 Codex,並提供詳細的示例和最佳實踐,以幫助開發者提高測試效率和代碼質量。通過使用子測試和基準測試,開發者可以組織相關測試和評估性能關鍵代碼,同時測試覆蓋率工具幫助確保代碼的全面測試。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/golang-testing

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Go 測試模式 是做什麼的?

全面的 Go 測試模式,用於遵循 TDD 方法論編寫可靠、可維護的測試。

何時啟用

  • 編寫新的 Go 函數或方法
  • 為現有代碼添加測試覆蓋率
  • 為性能關鍵代碼創建基準測試
  • 實現輸入驗證的模糊測試
  • 在 Go 項目中遵循 TDD 工作流程

Go 的 TDD 工作流程

RED-GREEN-REFACTOR 循環

RED     → 先編寫一個失敗的測試
GREEN   → 編寫最少代碼以通過測試
REFACTOR → 改進代碼,同時保持測試通過
REPEAT  → 繼續下一個需求

Go 中的逐步 TDD

// 步驟 1:定義接口/簽名
// calculator.go
package calculator

func Add(a, b int) int {
    panic("not implemented") // 佔位符
}

// 步驟 2:編寫失敗測試 (RED)
// calculator_test.go
package calculator

import "testing"

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2, 3) = %d; want %d", got, want)
    }
}

// 步驟 3:運行測試 - 驗證失敗
// $ go test
// --- FAIL: TestAdd (0.00s)
// panic: not implemented

// 步驟 4:實現最少代碼 (GREEN)
func Add(a, b int) int {
    return a + b
}

// 步驟 5:運行測試 - 驗證通過
// $ go test
// PASS

// 步驟 6:必要時重構,確保測試仍然通過

表格驅動測試

Go 測試的標準模式。以最少的代碼實現全面的覆蓋。

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"zero values", 0, 0, 0},
        {"mixed signs", -1, 1, 0},
        {"large numbers", 1000000, 2000000, 3000000},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, got, tt.expected)
            }
        })
    }
}

包含錯誤案例的表格驅動測試

func TestParseConfig(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    *Config
        wantErr bool
    }{
        {
            name:  "valid config",
            input: `{"host": "localhost", "port": 8080}`,
            want:  &Config{Host: "localhost", Port: 8080},
        },
        {
            name:    "invalid JSON",
            input:   `{invalid}`,
            wantErr: true,
        },
        {
            name:    "empty input",
            input:   "",
            wantErr: true,
        },
        {
            name:  "minimal config",
            input: `{}`,
            want:  &Config{}, // 零值配置
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := ParseConfig(tt.input)

            if tt.wantErr {
                if err == nil {
                    t.Error("expected error, got nil")
                }
                return
            }

            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }

            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("got %+v; want %+v", got, tt.want)
            }
        })
    }
}

子測試和子基準測試

組織相關測試

func TestUser(t *testing.T) {
    // 所有子測試共享的設置
    db := setupTestDB(t)

    t.Run("Create", func(t *testing.T) {
        user := &User{Name: "Alice"}
        err := db.CreateUser(user)
        if err != nil {
            t.Fatalf("CreateUser failed: %v", err)
        }
        if user.ID == "" {
            t.Error("expected user ID to be set")
        }
    })

    t.Run("Get", func(t *testing.T) {
        user, err := db.GetUser("alice-id")
        if err != nil {
            t.Fatalf("GetUser failed: %v", err)
        }
        if user.Name != "Alice" {
            t.Errorf("got name %q; want %q", user.Name, "Alice")
        }
    })

    t.Run("Update", func(t *testing.T) {
        // ...
    })

    t.Run("Delete", func(t *testing.T) {
        // ...
    })
}

並行子測試

func TestParallel(t *testing.T) {
    tests := []struct {
        name  string
        input string
    }{
        {"case1", "input1"},
        {"case2", "input2"},
        {"case3", "input3"},
    }

    for _, tt := range tests {
        tt := tt // 捕獲範圍變數
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // 並行運行子測試
            result := Process(tt.input)
            // 斷言...
            _ = result
        })
    }
}

測試輔助程式

輔助函數

func setupTestDB(t *testing.T) *sql.DB {
    t.Helper() // 標記此為輔助函數

    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("failed to open database: %v", err)
    }

    // 測試完成時清理
    t.Cleanup(func() {
        db.Close()
    })

    // 運行遷移
    if _, err := db.Exec(schema); err != nil {
        t.Fatalf("failed to create schema: %v", err)
    }

    return db
}

func assertNoError(t *testing.T, err error) {
    t.Helper()
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}

func assertEqual[T comparable](t *testing.T, got, want T) {
    t.Helper()
    if got != want {
        t.Errorf("got %v; want %v", got, want)
    }
}

臨時檔案和目錄

func TestFileProcessing(t *testing.T) {
    // 創建臨時目錄 - 自動清理
    tmpDir := t.TempDir()

    // 創建測試檔案
    testFile := filepath.Join(tmpDir, "test.txt")
    err := os.WriteFile(testFile, []byte("test content"), 0644)
    if err != nil {
        t.Fatalf("failed to create test file: %v", err)
    }

    // 運行測試
    result, err := ProcessFile(testFile)
    if err != nil {
        t.Fatalf("ProcessFile failed: %v", err)
    }

    // 斷言...
    _ = result
}

Golden 檔案

針對存儲在 testdata/ 中的預期輸出檔案進行測試。

var update = flag.Bool("update", false, "update golden files")

func TestRender(t *testing.T) {
    tests := []struct {
        name  string
        input Template
    }{
        {"simple", Template{Name: "test"}},
        {"complex", Template{Name: "test", Items: []string{"a", "b"}}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Render(tt.input)

            golden := filepath.Join("testdata", tt.name+".golden")

            if *update {
                // 更新 golden 檔案:go test -update
                err := os.WriteFile(golden, got, 0644)
                if err != nil {
                    t.Fatalf("failed to update golden file: %v", err)
                }
            }

            want, err := os.ReadFile(golden)
            if err != nil {
                t.Fatalf("failed to read golden file: %v", err)
            }

            if !bytes.Equal(got, want) {
                t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
            }
        })
    }
}

使用接口進行 Mock

基於接口的 Mock

// 為依賴定義接口
type UserRepository interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

// 生產實現
type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
    // 真實資料庫查詢
}

// 測試用的 Mock 實現
type MockUserRepository struct {
    GetUserFunc  func(id string) (*User, error)
    SaveUserFunc func(user *User) error
}

func (m *MockUserRepository) GetUser(id string) (*User, error) {
    return m.GetUserFunc(id)
}

func (m *MockUserRepository) SaveUser(user *User) error {
    return m.SaveUserFunc(user)
}

// 使用 Mock 的測試
func TestUserService(t *testing.T) {
    mock := &MockUserRepository{
        GetUserFunc: func(id string) (*User, error) {
            if id == "123" {
                return &User{ID: "123", Name: "Alice"}, nil
            }
            return nil, ErrNotFound
        },
    }

    service := NewUserService(mock)

    user, err := service.GetUserProfile("123")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("got name %q; want %q", user.Name, "Alice")
    }
}

基準測試

基本基準測試

func BenchmarkProcess(b *testing.B) {
    data := generateTestData(1000)
    b.ResetTimer() // 不計入設置時間

    for i := 0; i < b.N; i++ {
        Process(data)
    }
}

// 運行:go test -bench=BenchmarkProcess -benchmem
// 輸出:BenchmarkProcess-8   10000   105234 ns/op   4096 B/op   10 allocs/op

不同大小的基準測試

func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000, 100000}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
            data := generateRandomSlice(size)
            b.ResetTimer()

            for i := 0; i < b.N; i++ {
                // 製作副本以避免對已排序數據進行排序
                tmp := make([]int, len(data))
                copy(tmp, data)
                sort.Ints(tmp)
            }
        })
    }
}

記憶體分配基準測試

func BenchmarkStringConcat(b *testing.B) {
    parts := []string{"hello", "world", "foo", "bar", "baz"}

    b.Run("plus", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var s string
            for _, p := range parts {
                s += p
            }
            _ = s
        }
    })

    b.Run("builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            for _, p := range parts {
                sb.WriteString(p)
            }
            _ = sb.String()
        }
    })

    b.Run("join", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = strings.Join(parts, "")
        }
    })
}

模糊測試 (Go 1.18+)

基本模糊測試

func FuzzParseJSON(f *testing.F) {
    // 添加種子語料庫
    f.Add(`{"name": "test"}`)
    f.Add(`{"count": 123}`)
    f.Add(`[]`)
    f.Add(`""`)

    f.Fuzz(func(t *testing.T, input string) {
        var result map[string]interface{}
        err := json.Unmarshal([]byte(input), &result)

        if err != nil {
            // 對於隨機輸入,無效 JSON 是可預期的
            return
        }

        // 如果解析成功,重新編碼應正常工作
        _, err = json.Marshal(result)
        if err != nil {
            t.Errorf("Marshal failed after successful Unmarshal: %v", err)
        }
    })
}

// 運行:go test -fuzz=FuzzParseJSON -fuzztime=30s

多輸入模糊測試

func FuzzCompare(f *testing.F) {
    f.Add("hello", "world")
    f.Add("", "")
    f.Add("abc", "abc")

    f.Fuzz(func(t *testing.T, a, b string) {
        result := Compare(a, b)

        // 屬性:Compare(a, a) 應始終等於 0
        if a == b && result != 0 {
            t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
        }

        // 屬性:Compare(a, b) 和 Compare(b, a) 應具有相反的符號
        reverse := Compare(b, a)
        if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
            if result != 0 || reverse != 0 {
                t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
                    a, b, result, b, a, reverse)
            }
        }
    })
}

測試覆蓋率

運行覆蓋率

# 基本覆蓋率
go test -cover ./...

# 生成覆蓋率檔案
go test -coverprofile=coverage.out ./...

# 在瀏覽器中查看覆蓋率
go tool cover -html=coverage.out

# 按函數查看覆蓋率
go tool cover -func=coverage.out

# 帶競爭檢測的覆蓋率
go test -race -coverprofile=coverage.out ./...

覆蓋率目標

代碼類型目標
關鍵業務邏輯100%
公共 API90%+
一般代碼80%+
生成的代碼排除

從覆蓋率中排除生成的代碼

//go:generate mockgen -source=interface.go -destination=mock_interface.go

// 在覆蓋率配置檔中,使用構建標籤排除:
// go test -cover -tags=!generate ./...

HTTP 處理器測試

func TestHealthHandler(t *testing.T) {
    // 創建請求
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    w := httptest.NewRecorder()

    // 調用處理器
    HealthHandler(w, req)

    // 檢查響應
    resp := w.Result()
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
    }

    body, _ := io.ReadAll(resp.Body)
    if string(body) != "OK" {
        t.Errorf("got body %q; want %q", body, "OK")
    }
}

func TestAPIHandler(t *testing.T) {
    tests := []struct {
        name       string
        method     string
        path       string
        body       string
        wantStatus int
        wantBody   string
    }{
        {
            name:       "get user",
            method:     http.MethodGet,
            path:       "/users/123",
            wantStatus: http.StatusOK,
            wantBody:   `{"id":"123","name":"Alice"}`,
        },
        {
            name:       "not found",
            method:     http.MethodGet,
            path:       "/users/999",
            wantStatus: http.StatusNotFound,
        },
        {
            name:       "create user",
            method:     http.MethodPost,
            path:       "/users",
            body:       `{"name":"Bob"}`,
            wantStatus: http.StatusCreated,
        },
    }

    handler := NewAPIHandler()

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            var body io.Reader
            if tt.body != "" {
                body = strings.NewReader(tt.body)
            }

            req := httptest.NewRequest(tt.method, tt.path, body)
            w := httptest.NewRecorder()

            handler.ServeHTTP(w, req)

            resp := w.Result()
            defer resp.Body.Close()

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

            if tt.wantBody != "" {
                gotBody, _ := io.ReadAll(resp.Body)
                if string(gotBody) != tt.wantBody {
                    t.Errorf("got body %q; want %q", string(gotBody), tt.wantBody)
                }
            }
        })
    }
}

測試運維工具

func TestMain(m *testing.M) {
    // 所有測試的設置
    setup()

    // 運行測試
    code := m.Run()

    // 所有測試的清理
    teardown()

    os.Exit(code)
}

清理資源

func TestWithCleanup(t *testing.T) {
    // 在函數啟動時註冊清理
    t.Cleanup(func() {
        fmt.Println("cleanup called")
    })

    // 測試代碼...
    // 即使測試失敗,清理也會被調用
}

最佳實踐

測試檔案命名

  • 將測試放在 *_test.go 檔案中
  • 為整合測試使用 package_test 測試包
  • 為白盒測試使用 package 測試包
  • 將測試輔助程式放在 testutil/ 或 testhelpers/ 中

測試組織

  • 每種函數/方法一個 Test 函數
  • 對於邊界案例和不同場景使用表格驅動測試
  • 對於相關功能使用子測試
  • 將測試數據放在 testdata/ 目錄中
  • 對預期輸出使用 Golden 檔案

斷言

  • 使用 t.Error 進行非致命斷言
  • 使用 t.Fatal/t.Fatalf 進行致命斷言
  • 當測試無法繼續時停止測試
  • 在錯誤消息中提供語境資訊
  • 對自定義斷言使用 t.Helper()

測試覆蓋

  • 針對關鍵路徑目標達到 100% 覆蓋率
  • 為公共 API 目標達到 90% 以上覆蓋率
  • 為一般代碼目標達到 80% 以上覆蓋率
  • 定期運行 go test -cover 以監控
  • 使用 go tool cover -html 找出未覆蓋的區域

性能

  • 為性能關鍵代碼編寫基準測試
  • 注意基準測試中的分配
  • 使用 -benchmem 標誌分析記憶體
  • 對並行子測試使用 t.Parallel()
  • 使用 b.ResetTimer() 排除設置時間

常見陷阱與解決方案

忽略錯誤

// 不好:忽略錯誤
json.Unmarshal(data, &result)

// 好:總是檢查錯誤
if err := json.Unmarshal(data, &result); err != nil {
    t.Fatalf("failed to unmarshal: %v", err)
}

競態條件

// 不好:並行測試中的共享狀態
for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        t.Parallel()
        // 在循環變數上存在競態條件
    })
}

// 好:捕獲變數
for _, tt := range tests {
    tt := tt
    t.Run(tt.name, func(t *testing.T) {
        t.Parallel()
        // 使用捕獲的變數
    })
}

全局狀態

// 不好:依賴全局狀態
func TestSomething(t *testing.T) {
    result := GlobalFunction()
    // 可能受其他測試影響
}

// 好:隔離依賴
func TestSomething(t *testing.T) {
    svc := NewService() // 新鮮實例
    result := svc.DoSomething()
}

相關測試命令

# 運行所有測試
go test ./...

# 運行測試並顯示詳細輸出
go test -v ./...

# 運行特定測試函數
go test -run TestAdd ./...

# 運行符合模式匹配的測試
go test -run "TestAdd/positive" ./...

# 運行基準測試
go test -bench=. ./...

# 運行帶覆蓋率的測試
go test -cover ./...

# 運行模糊測試
go test -fuzz=FuzzFunc -fuzztime=30s

# 運行帶競態檢測的測試
go test -race ./...

# 查看測試覆蓋率報告
go tool cover -html=coverage.out

# 檢查測試性能
go test -bench=. -benchmem ./...

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

相關技能