golang-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Testing Patterns

Go语言测试模式

Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology.
一套全面的Go语言测试模式,用于遵循TDD方法论编写可靠、可维护的测试用例。

When to Activate

适用场景

  • Writing new Go functions or methods
  • Adding test coverage to existing code
  • Creating benchmarks for performance-critical code
  • Implementing fuzz tests for input validation
  • Following TDD workflow in Go projects
  • 编写新的Go函数或方法
  • 为现有代码添加测试覆盖率
  • 为性能关键型代码创建基准测试
  • 为输入验证实现模糊测试
  • 在Go项目中遵循TDD工作流

TDD Workflow for Go

Go语言的TDD工作流

The RED-GREEN-REFACTOR Cycle

RED-GREEN-REFACTOR 循环

RED     → Write a failing test first
GREEN   → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT  → Continue with next requirement
RED     → 先编写失败的测试用例
GREEN   → 编写最少代码使测试通过
REFACTOR → 在保持测试通过的同时优化代码
REPEAT  → 继续处理下一个需求

Step-by-Step TDD in Go

Go语言TDD分步指南

go
// Step 1: Define the interface/signature
// calculator.go
package calculator

func Add(a, b int) int {
    panic("not implemented") // Placeholder
}

// Step 2: Write failing test (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)
    }
}

// Step 3: Run test - verify FAIL
// $ go test
// --- FAIL: TestAdd (0.00s)
// panic: not implemented

// Step 4: Implement minimal code (GREEN)
func Add(a, b int) int {
    return a + b
}

// Step 5: Run test - verify PASS
// $ go test
// PASS

// Step 6: Refactor if needed, verify tests still pass
go
// Step 1: 定义接口/签名
// calculator.go
package calculator

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

// Step 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)
    }
}

// Step 3: 运行测试 - 确认失败
// $ go test
// --- FAIL: TestAdd (0.00s)
// panic: not implemented

// Step 4: 实现最少代码(GREEN阶段)
func Add(a, b int) int {
    return a + b
}

// Step 5: 运行测试 - 确认通过
// $ go test
// PASS

// Step 6: 按需重构,确认测试仍通过

Table-Driven Tests

表格驱动测试

The standard pattern for Go tests. Enables comprehensive coverage with minimal code.
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)
            }
        })
    }
}
Go语言测试的标准模式,用最少代码实现全面覆盖。
go
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"正数相加", 2, 3, 5},
        {"负数相加", -1, -2, -3},
        {"零值相加", 0, 0, 0},
        {"正负混合", -1, 1, 0},
        {"大数相加", 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)
            }
        })
    }
}

Table-Driven Tests with Error Cases

包含错误场景的表格驱动测试

go
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{}, // Zero value 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)
            }
        })
    }
}
go
func TestParseConfig(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        want    *Config
        wantErr bool
    }{
        {
            name:  "合法配置",
            input: `{"host": "localhost", "port": 8080}`,
            want:  &Config{Host: "localhost", Port: 8080},
        },
        {
            name:    "无效JSON",
            input:   `{invalid}`,
            wantErr: true,
        },
        {
            name:    "空输入",
            input:   "",
            wantErr: true,
        },
        {
            name:  "最简配置",
            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("预期返回错误,但未得到错误")
                }
                return
            }

            if err != nil {
                t.Fatalf("意外错误: %v", err)
            }

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

Subtests and Sub-benchmarks

子测试与子基准测试

Organizing Related Tests

组织相关测试用例

go
func TestUser(t *testing.T) {
    // Setup shared by all subtests
    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) {
        // ...
    })
}
go
func TestUser(t *testing.T) {
    // 所有子测试共享的初始化操作
    db := setupTestDB(t)

    t.Run("创建用户", func(t *testing.T) {
        user := &User{Name: "Alice"}
        err := db.CreateUser(user)
        if err != nil {
            t.Fatalf("CreateUser 失败: %v", err)
        }
        if user.ID == "" {
            t.Error("预期用户ID会被赋值")
        }
    })

    t.Run("获取用户", func(t *testing.T) {
        user, err := db.GetUser("alice-id")
        if err != nil {
            t.Fatalf("GetUser 失败: %v", err)
        }
        if user.Name != "Alice" {
            t.Errorf("得到名称 %q; 预期 %q", user.Name, "Alice")
        }
    })

    t.Run("更新用户", func(t *testing.T) {
        // ...
    })

    t.Run("删除用户", func(t *testing.T) {
        // ...
    })
}

Parallel Subtests

并行子测试

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

    for _, tt := range tests {
        tt := tt // Capture range variable
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // Run subtests in parallel
            result := Process(tt.input)
            // assertions...
            _ = result
        })
    }
}
go
func TestParallel(t *testing.T) {
    tests := []struct {
        name  string
        input string
    }{
        {"用例1", "输入1"},
        {"用例2", "输入2"},
        {"用例3", "输入3"},
    }

    for _, tt := range tests {
        tt := tt // 捕获循环变量
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // 并行运行子测试
            result := Process(tt.input)
            // 断言逻辑...
            _ = result
        })
    }
}

Test Helpers

测试辅助工具

Helper Functions

辅助函数

go
func setupTestDB(t *testing.T) *sql.DB {
    t.Helper() // Marks this as a helper function

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

    // Cleanup when test finishes
    t.Cleanup(func() {
        db.Close()
    })

    // Run migrations
    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)
    }
}
go
func setupTestDB(t *testing.T) *sql.DB {
    t.Helper() // 将此标记为辅助函数

    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("打开数据库失败: %v", err)
    }

    // 测试结束时清理资源
    t.Cleanup(func() {
        db.Close()
    })

    // 运行迁移脚本
    if _, err := db.Exec(schema); err != nil {
        t.Fatalf("创建 schema 失败: %v", err)
    }

    return db
}

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

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

Temporary Files and Directories

临时文件与目录

go
func TestFileProcessing(t *testing.T) {
    // Create temp directory - automatically cleaned up
    tmpDir := t.TempDir()

    // Create test file
    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)
    }

    // Run test
    result, err := ProcessFile(testFile)
    if err != nil {
        t.Fatalf("ProcessFile failed: %v", err)
    }

    // Assert...
    _ = result
}
go
func TestFileProcessing(t *testing.T) {
    // 创建临时目录 - 会自动清理
    tmpDir := t.TempDir()

    // 创建测试文件
    testFile := filepath.Join(tmpDir, "test.txt")
    err := os.WriteFile(testFile, []byte("测试内容"), 0644)
    if err != nil {
        t.Fatalf("创建测试文件失败: %v", err)
    }

    // 运行测试
    result, err := ProcessFile(testFile)
    if err != nil {
        t.Fatalf("ProcessFile 失败: %v", err)
    }

    // 断言逻辑...
    _ = result
}

Golden Files

黄金文件测试

Testing against expected output files stored in
testdata/
.
go
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 {
                // Update golden file: 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)
            }
        })
    }
}
基于存储在
testdata/
目录中的预期输出文件进行测试。
go
var update = flag.Bool("update", false, "更新黄金文件")

func TestRender(t *testing.T) {
    tests := []struct {
        name  string
        input Template
    }{
        {"简单模板", Template{Name: "test"}},
        {"复杂模板", 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 {
                // 更新黄金文件: go test -update
                err := os.WriteFile(golden, got, 0644)
                if err != nil {
                    t.Fatalf("更新黄金文件失败: %v", err)
                }
            }

            want, err := os.ReadFile(golden)
            if err != nil {
                t.Fatalf("读取黄金文件失败: %v", err)
            }

            if !bytes.Equal(got, want) {
                t.Errorf("输出不匹配:\n得到:\n%s\n预期:\n%s", got, want)
            }
        })
    }
}

Mocking with Interfaces

基于接口的Mock测试

Interface-Based Mocking

接口驱动的Mock实现

go
// Define interface for dependencies
type UserRepository interface {
    GetUser(id string) (*User, error)
    SaveUser(user *User) error
}

// Production implementation
type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
    // Real database query
}

// Mock implementation for tests
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)
}

// Test using 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")
    }
}
go
// 定义依赖的接口
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("意外错误: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("得到名称 %q; 预期 %q", user.Name, "Alice")
    }
}

Benchmarks

基准测试

Basic Benchmarks

基础基准测试

go
func BenchmarkProcess(b *testing.B) {
    data := generateTestData(1000)
    b.ResetTimer() // Don't count setup time

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

// Run: go test -bench=BenchmarkProcess -benchmem
// Output: BenchmarkProcess-8   10000   105234 ns/op   4096 B/op   10 allocs/op
go
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

Benchmark with Different Sizes

不同数据规模的基准测试

go
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++ {
                // Make a copy to avoid sorting already sorted data
                tmp := make([]int, len(data))
                copy(tmp, data)
                sort.Ints(tmp)
            }
        })
    }
}
go
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)
            }
        })
    }
}

Memory Allocation Benchmarks

内存分配基准测试

go
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
func BenchmarkStringConcat(b *testing.B) {
    parts := []string{"hello", "world", "foo", "bar", "baz"}

    b.Run("加号拼接", 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, "")
        }
    })
}

Fuzzing (Go 1.18+)

模糊测试(Go 1.18+)

Basic Fuzz Test

基础模糊测试

go
func FuzzParseJSON(f *testing.F) {
    // Add seed corpus
    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 {
            // Invalid JSON is expected for random input
            return
        }

        // If parsing succeeded, re-encoding should work
        _, err = json.Marshal(result)
        if err != nil {
            t.Errorf("Marshal failed after successful Unmarshal: %v", err)
        }
    })
}

// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s
go
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("Unmarshal成功后Marshal失败: %v", err)
        }
    })
}

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

Fuzz Test with Multiple Inputs

多输入模糊测试

go
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)

        // Property: Compare(a, a) should always equal 0
        if a == b && result != 0 {
            t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
        }

        // Property: Compare(a, b) and Compare(b, a) should have opposite signs
        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
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; 预期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; 结果不一致",
                    a, b, result, b, a, reverse)
            }
        }
    })
}

Test Coverage

测试覆盖率

Running Coverage

运行覆盖率统计

bash
undefined
bash
undefined

Basic coverage

基础覆盖率统计

go test -cover ./...
go test -cover ./...

Generate coverage profile

生成覆盖率报告文件

go test -coverprofile=coverage.out ./...
go test -coverprofile=coverage.out ./...

View coverage in browser

在浏览器中查看覆盖率

go tool cover -html=coverage.out
ngo tool cover -html=coverage.out

View coverage by function

按函数查看覆盖率

go tool cover -func=coverage.out
go tool cover -func=coverage.out

Coverage with race detection

结合竞态检测的覆盖率统计

go test -race -coverprofile=coverage.out ./...
undefined
go test -race -coverprofile=coverage.out ./...
undefined

Coverage Targets

覆盖率目标

Code TypeTarget
Critical business logic100%
Public APIs90%+
General code80%+
Generated codeExclude
代码类型目标覆盖率
核心业务逻辑100%
公共API90%+
通用代码80%+
自动生成代码排除

Excluding Generated Code from Coverage

排除自动生成代码的覆盖率统计

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

// In coverage profile, exclude with build tags:
// go test -cover -tags=!generate ./...
go
//go:generate mockgen -source=interface.go -destination=mock_interface.go

// 在覆盖率报告中,通过构建标签排除:
// go test -cover -tags=!generate ./...

HTTP Handler Testing

HTTP处理器测试

go
func TestHealthHandler(t *testing.T) {
    // Create request
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    w := httptest.NewRecorder()

    // Call handler
    HealthHandler(w, req)

    // Check response
    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)
            req.Header.Set("Content-Type", "application/json")
            w := httptest.NewRecorder()

            handler.ServeHTTP(w, req)

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

            if tt.wantBody != "" && w.Body.String() != tt.wantBody {
                t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
            }
        })
    }
}
go
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("得到状态码 %d; 预期 %d", resp.StatusCode, http.StatusOK)
    }

    body, _ := io.ReadAll(resp.Body)
    if string(body) != "OK" {
        t.Errorf("得到响应体 %q; 预期 %q", body, "OK")
    }
}

func TestAPIHandler(t *testing.T) {
    tests := []struct {
        name       string
        method     string
        path       string
        body       string
        wantStatus int
        wantBody   string
    }{
        {
            name:       "获取用户",
            method:     http.MethodGet,
            path:       "/users/123",
            wantStatus: http.StatusOK,
            wantBody:   `{"id":"123","name":"Alice"}`,
        },
        {
            name:       "用户不存在",
            method:     http.MethodGet,
            path:       "/users/999",
            wantStatus: http.StatusNotFound,
        },
        {
            name:       "创建用户",
            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)
            req.Header.Set("Content-Type", "application/json")
            w := httptest.NewRecorder()

            handler.ServeHTTP(w, req)

            if w.Code != tt.wantStatus {
                t.Errorf("得到状态码 %d; 预期 %d", w.Code, tt.wantStatus)
            }

            if tt.wantBody != "" && w.Body.String() != tt.wantBody {
                t.Errorf("得到响应体 %q; 预期 %q", w.Body.String(), tt.wantBody)
            }
        })
    }
}

Testing Commands

测试命令汇总

bash
undefined
bash
undefined

Run all tests

运行所有测试

go test ./...
go test ./...

Run tests with verbose output

运行测试并输出详细日志

go test -v ./...
go test -v ./...

Run specific test

运行指定测试

go test -run TestAdd ./...
go test -run TestAdd ./...

Run tests matching pattern

运行匹配指定模式的测试

go test -run "TestUser/Create" ./...
go test -run "TestUser/Create" ./...

Run tests with race detector

运行测试并启用竞态检测

go test -race ./...
go test -race ./...

Run tests with coverage

运行测试并统计覆盖率

go test -cover -coverprofile=coverage.out ./...
go test -cover -coverprofile=coverage.out ./...

Run short tests only

仅运行短测试

go test -short ./...
go test -short ./...

Run tests with timeout

为测试设置超时

go test -timeout 30s ./...
go test -timeout 30s ./...

Run benchmarks

运行基准测试

go test -bench=. -benchmem ./...
go test -bench=. -benchmem ./...

Run fuzzing

运行模糊测试

go test -fuzz=FuzzParse -fuzztime=30s ./...
go test -fuzz=FuzzParse -fuzztime=30s ./...

Count test runs (for flaky test detection)

重复运行测试(检测不稳定测试)

go test -count=10 ./...
undefined
go test -count=10 ./...
undefined

Best Practices

最佳实践

DO:
  • Write tests FIRST (TDD)
  • Use table-driven tests for comprehensive coverage
  • Test behavior, not implementation
  • Use
    t.Helper()
    in helper functions
  • Use
    t.Parallel()
    for independent tests
  • Clean up resources with
    t.Cleanup()
  • Use meaningful test names that describe the scenario
DON'T:
  • Test private functions directly (test through public API)
  • Use
    time.Sleep()
    in tests (use channels or conditions)
  • Ignore flaky tests (fix or remove them)
  • Mock everything (prefer integration tests when possible)
  • Skip error path testing
建议:
  • 先编写测试用例(TDD)
  • 使用表格驱动测试实现全面覆盖
  • 测试行为而非实现细节
  • 在辅助函数中使用
    t.Helper()
  • 对独立测试用例使用
    t.Parallel()
  • 使用
    t.Cleanup()
    清理资源
  • 使用有意义的测试名称描述场景
避免:
  • 直接测试私有函数(通过公共API间接测试)
  • 在测试中使用
    time.Sleep()
    (使用通道或条件判断替代)
  • 忽略不稳定测试(修复或移除)
  • 对所有依赖都做Mock(可能的话优先选择集成测试)
  • 跳过错误路径的测试

Integration with CI/CD

与CI/CD集成

yaml
undefined
yaml
undefined

GitHub Actions example

GitHub Actions 示例

test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.22'
- name: Run tests
  run: go test -race -coverprofile=coverage.out ./...

- name: Check coverage
  run: |
    go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
    awk -F'%' '{if ($1 < 80) exit 1}'

**Remember**: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.
test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.22'
- name: 运行测试
  run: go test -race -coverprofile=coverage.out ./...

- name: 检查覆盖率
  run: |
    go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
    awk -F'%' '{if ($1 < 80) exit 1}'

**提示**: 测试用例也是文档。它们展示了代码的预期使用方式。请清晰编写并保持更新。