golang-testing-strategies

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Testing Strategies

Go测试策略

Overview

概述

Go provides a robust built-in testing framework (
testing
package) that emphasizes simplicity and developer productivity. Combined with community tools like testify and gomock, Go testing enables comprehensive test coverage with minimal boilerplate.
Key Features:
  • 📋 Table-Driven Tests: Idiomatic pattern for testing multiple inputs
  • Testify: Readable assertions and test suites
  • 🎭 Gomock: Type-safe interface mocking
  • Benchmarking: Built-in performance testing
  • 🔍 Race Detector: Concurrent code safety verification
  • 📊 Coverage: Native coverage reporting and enforcement
  • 🚀 CI Integration: Test caching and parallel execution
Go提供了一个强大的内置测试框架(
testing
包),强调简洁性和开发者生产力。结合Testify、Gomock等社区工具,Go测试能够以极少的样板代码实现全面的测试覆盖。
核心特性:
  • 📋 表驱动测试: 用于测试多组输入的惯用模式
  • Testify: 可读性强的断言和测试套件
  • 🎭 Gomock: 类型安全的接口模拟
  • 基准测试: 内置的性能测试
  • 🔍 竞态检测器: 并发代码安全验证
  • 📊 覆盖率: 原生的覆盖率报告与强制检查
  • 🚀 CI集成: 测试缓存与并行执行

When to Use This Skill

何时使用该技能

Activate this skill when:
  • Writing test suites for Go libraries or applications
  • Setting up testing infrastructure for new projects
  • Mocking external dependencies (databases, APIs, services)
  • Benchmarking performance-critical code paths
  • Ensuring thread-safe concurrent implementations
  • Integrating tests into CI/CD pipelines
  • Migrating from other testing frameworks
在以下场景中启用此技能:
  • 为Go库或应用编写测试套件
  • 为新项目搭建测试基础设施
  • 模拟外部依赖(数据库、API、服务)
  • 对性能关键代码路径进行基准测试
  • 确保线程安全的并发实现
  • 将测试集成到CI/CD流水线
  • 从其他测试框架迁移

Core Testing Principles

核心测试原则

The Go Testing Philosophy

Go测试理念

  1. Simplicity Over Magic: Use standard library when possible
  2. Table-Driven Tests: Test multiple scenarios with single function
  3. Subtests: Organize related tests with
    t.Run()
  4. Interface-Based Mocking: Mock dependencies through interfaces
  5. Test Files Colocate: Place
    *_test.go
    files alongside code
  6. Package Naming: Use
    package_test
    for external tests,
    package
    for internal
  1. 简洁优先,避免魔法: 尽可能使用标准库
  2. 表驱动测试: 用单个函数测试多个场景
  3. 子测试: 使用
    t.Run()
    组织相关测试
  4. 基于接口的模拟: 通过接口模拟依赖
  5. 测试文件同目录放置: 将
    *_test.go
    文件与代码放在一起
  6. 包命名: 外部测试使用
    package_test
    ,内部测试使用原包名

Test Organization

测试组织

File Naming Convention:
  • Unit tests:
    file_test.go
  • Integration tests:
    file_integration_test.go
  • Benchmark tests: Prefix with
    Benchmark
    in same test file
Package Structure:
mypackage/
├── user.go
├── user_test.go              // Internal tests (same package)
├── user_external_test.go     // External tests (package mypackage_test)
├── integration_test.go       // Integration tests
└── testdata/                 // Test fixtures (ignored by go build)
    └── golden.json
文件命名规范:
  • 单元测试:
    file_test.go
  • 集成测试:
    file_integration_test.go
  • 基准测试: 在同一测试文件中以
    Benchmark
    为前缀
包结构:
mypackage/
├── user.go
├── user_test.go              // 内部测试(同包)
├── user_external_test.go     // 外部测试(包名为mypackage_test)
├── integration_test.go       // 集成测试
└── testdata/                 // 测试夹具(go build会忽略)
    └── golden.json

Table-Driven Test Pattern

表驱动测试模式

Basic Structure

基本结构

The idiomatic Go testing pattern for testing multiple inputs:
go
func TestUserValidation(t *testing.T) {
    tests := []struct {
        name    string
        input   User
        wantErr bool
        errMsg  string
    }{
        {
            name:    "valid user",
            input:   User{Name: "Alice", Age: 30, Email: "alice@example.com"},
            wantErr: false,
        },
        {
            name:    "empty name",
            input:   User{Name: "", Age: 30, Email: "alice@example.com"},
            wantErr: true,
            errMsg:  "name is required",
        },
        {
            name:    "invalid email",
            input:   User{Name: "Bob", Age: 25, Email: "invalid"},
            wantErr: true,
            errMsg:  "invalid email format",
        },
        {
            name:    "negative age",
            input:   User{Name: "Charlie", Age: -5, Email: "charlie@example.com"},
            wantErr: true,
            errMsg:  "age must be positive",
        },
    }

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

            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
                return
            }

            if tt.wantErr && err.Error() != tt.errMsg {
                t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
            }
        })
    }
}
Go中用于测试多组输入的惯用模式:
go
func TestUserValidation(t *testing.T) {
    tests := []struct {
        name    string
        input   User
        wantErr bool
        errMsg  string
    }{
        {
            name:    "valid user",
            input:   User{Name: "Alice", Age: 30, Email: "alice@example.com"},
            wantErr: false,
        },
        {
            name:    "empty name",
            input:   User{Name: "", Age: 30, Email: "alice@example.com"},
            wantErr: true,
            errMsg:  "name is required",
        },
        {
            name:    "invalid email",
            input:   User{Name: "Bob", Age: 25, Email: "invalid"},
            wantErr: true,
            errMsg:  "invalid email format",
        },
        {
            name:    "negative age",
            input:   User{Name: "Charlie", Age: -5, Email: "charlie@example.com"},
            wantErr: true,
            errMsg:  "age must be positive",
        },
    }

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

            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
                return
            }

            if tt.wantErr && err.Error() != tt.errMsg {
                t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
            }
        })
    }
}

Parallel Test Execution

并行测试执行

Enable parallel test execution for independent tests:
go
func TestConcurrentOperations(t *testing.T) {
    tests := []struct {
        name string
        fn   func() int
        want int
    }{
        {"operation 1", func() int { return compute1() }, 42},
        {"operation 2", func() int { return compute2() }, 84},
        {"operation 3", func() int { return compute3() }, 126},
    }

    for _, tt := range tests {
        tt := tt // Capture range variable
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // Run tests concurrently

            got := tt.fn()
            if got != tt.want {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}
为独立测试启用并行执行:
go
func TestConcurrentOperations(t *testing.T) {
    tests := []struct {
        name string
        fn   func() int
        want int
    }{
        {"operation 1", func() int { return compute1() }, 42},
        {"operation 2", func() int { return compute2() }, 84},
        {"operation 3", func() int { return compute3() }, 126},
    }

    for _, tt := range tests {
        tt := tt // 捕获循环变量
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // 并发运行测试

            got := tt.fn()
            if got != tt.want {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}

Testify Framework

Testify框架

Installation

安装

bash
go get github.com/stretchr/testify
bash
go get github.com/stretchr/testify

Assertions

断言

Replace verbose error checking with readable assertions:
go
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestCalculator(t *testing.T) {
    calc := NewCalculator()

    // assert: Test continues on failure
    assert.Equal(t, 5, calc.Add(2, 3))
    assert.NotNil(t, calc)
    assert.True(t, calc.IsReady())

    // require: Test stops on failure (for critical assertions)
    result, err := calc.Divide(10, 2)
    require.NoError(t, err) // Stop if error occurs
    assert.Equal(t, 5, result)
}

func TestUserOperations(t *testing.T) {
    user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}

    // Object matching
    assert.Equal(t, 1, user.ID)
    assert.Contains(t, user.Email, "@")
    assert.Len(t, user.Name, 5)

    // Partial matching
    assert.ObjectsAreEqual(user, &User{
        ID:    1,
        Name:  "Alice",
        Email: assert.AnythingOfType("string"),
    })
}
用可读性强的断言替代冗长的错误检查:
go
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestCalculator(t *testing.T) {
    calc := NewCalculator()

    // assert: 测试失败后继续执行
    assert.Equal(t, 5, calc.Add(2, 3))
    assert.NotNil(t, calc)
    assert.True(t, calc.IsReady())

    // require: 测试失败后立即停止(用于关键断言)
    result, err := calc.Divide(10, 2)
    require.NoError(t, err) // 若出现错误则停止
    assert.Equal(t, 5, result)
}

func TestUserOperations(t *testing.T) {
    user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}

    // 对象匹配
    assert.Equal(t, 1, user.ID)
    assert.Contains(t, user.Email, "@")
    assert.Len(t, user.Name, 5)

    // 部分匹配
    assert.ObjectsAreEqual(user, &User{
        ID:    1,
        Name:  "Alice",
        Email: assert.AnythingOfType("string"),
    })
}

Test Suites

测试套件

Organize related tests with setup/teardown:
go
import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type UserServiceTestSuite struct {
    suite.Suite
    db      *sql.DB
    service *UserService
}

// SetupSuite runs once before all tests
func (s *UserServiceTestSuite) SetupSuite() {
    s.db = setupTestDatabase()
    s.service = NewUserService(s.db)
}

// TearDownSuite runs once after all tests
func (s *UserServiceTestSuite) TearDownSuite() {
    s.db.Close()
}

// SetupTest runs before each test
func (s *UserServiceTestSuite) SetupTest() {
    cleanDatabase(s.db)
}

// TearDownTest runs after each test
func (s *UserServiceTestSuite) TearDownTest() {
    // Cleanup if needed
}

// Test methods must start with "Test"
func (s *UserServiceTestSuite) TestCreateUser() {
    user := &User{Name: "Alice", Email: "alice@example.com"}

    err := s.service.Create(user)
    s.NoError(err)
    s.NotEqual(0, user.ID) // ID assigned
}

func (s *UserServiceTestSuite) TestGetUser() {
    // Setup
    user := &User{Name: "Bob", Email: "bob@example.com"}
    s.service.Create(user)

    // Test
    retrieved, err := s.service.GetByID(user.ID)
    s.NoError(err)
    s.Equal(user.Name, retrieved.Name)
}

// Run the suite
func TestUserServiceTestSuite(t *testing.T) {
    suite.Run(t, new(UserServiceTestSuite))
}
通过初始化/清理步骤组织相关测试:
go
import (
    "testing"
    "github.com/stretchr/testify/suite"
)

type UserServiceTestSuite struct {
    suite.Suite
    db      *sql.DB
    service *UserService
}

// SetupSuite在所有测试前运行一次
func (s *UserServiceTestSuite) SetupSuite() {
    s.db = setupTestDatabase()
    s.service = NewUserService(s.db)
}

// TearDownSuite在所有测试后运行一次
func (s *UserServiceTestSuite) TearDownSuite() {
    s.db.Close()
}

// SetupTest在每个测试前运行
func (s *UserServiceTestSuite) SetupTest() {
    cleanDatabase(s.db)
}

// TearDownTest在每个测试后运行
func (s *UserServiceTestSuite) TearDownTest() {
    // 如有需要进行清理
}

// 测试方法必须以"Test"开头
func (s *UserServiceTestSuite) TestCreateUser() {
    user := &User{Name: "Alice", Email: "alice@example.com"}

    err := s.service.Create(user)
    s.NoError(err)
    s.NotEqual(0, user.ID) // ID已分配
}

func (s *UserServiceTestSuite) TestGetUser() {
    // 初始化
    user := &User{Name: "Bob", Email: "bob@example.com"}
    s.service.Create(user)

    // 测试
    retrieved, err := s.service.GetByID(user.ID)
    s.NoError(err)
    s.Equal(user.Name, retrieved.Name)
}

// 运行测试套件
func TestUserServiceTestSuite(t *testing.T) {
    suite.Run(t, new(UserServiceTestSuite))
}

Gomock Interface Mocking

Gomock接口模拟

Installation

安装

bash
go install github.com/golang/mock/mockgen@latest
bash
go install github.com/golang/mock/mockgen@latest

Generate Mocks

生成模拟对象

go
// user_repository.go
package repository

//go:generate mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks

type UserRepository interface {
    GetByID(id int) (*User, error)
    Create(user *User) error
    Update(user *User) error
    Delete(id int) error
}
Generate mocks:
bash
go generate ./...
go
// user_repository.go
package repository

//go:generate mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks

type UserRepository interface {
    GetByID(id int) (*User, error)
    Create(user *User) error
    Update(user *User) error
    Delete(id int) error
}
生成模拟对象:
bash
go generate ./...

Or manually:

或手动执行:

mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
undefined
mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
undefined

Using Mocks in Tests

在测试中使用模拟对象

go
import (
    "testing"
    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
    "myapp/repository/mocks"
)

func TestUserService_GetUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    // Create mock
    mockRepo := mocks.NewMockUserRepository(ctrl)

    // Set expectations
    expectedUser := &User{ID: 1, Name: "Alice"}
    mockRepo.EXPECT().
        GetByID(1).
        Return(expectedUser, nil).
        Times(1)

    // Test
    service := NewUserService(mockRepo)
    user, err := service.GetUser(1)

    // Assertions
    assert.NoError(t, err)
    assert.Equal(t, expectedUser, user)
}

func TestUserService_CreateUser_Validation(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockUserRepository(ctrl)

    // Expect Create to NOT be called (validation should fail first)
    mockRepo.EXPECT().Create(gomock.Any()).Times(0)

    service := NewUserService(mockRepo)
    err := service.CreateUser(&User{Name: ""}) // Invalid user

    assert.Error(t, err)
    assert.Contains(t, err.Error(), "name is required")
}
go
import (
    "testing"
    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
    "myapp/repository/mocks"
)

func TestUserService_GetUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    // 创建模拟对象
    mockRepo := mocks.NewMockUserRepository(ctrl)

    // 设置预期调用
    expectedUser := &User{ID: 1, Name: "Alice"}
    mockRepo.EXPECT().
        GetByID(1).
        Return(expectedUser, nil).
        Times(1)

    // 测试
    service := NewUserService(mockRepo)
    user, err := service.GetUser(1)

    // 断言
    assert.NoError(t, err)
    assert.Equal(t, expectedUser, user)
}

func TestUserService_CreateUser_Validation(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockUserRepository(ctrl)

    // 预期Create不会被调用(验证应先失败)
    mockRepo.EXPECT().Create(gomock.Any()).Times(0)

    service := NewUserService(mockRepo)
    err := service.CreateUser(&User{Name: ""}) // 无效用户

    assert.Error(t, err)
    assert.Contains(t, err.Error(), "name is required")
}

Custom Matchers

自定义匹配器

go
// Custom matcher for complex validation
type userMatcher struct {
    expectedEmail string
}

func (m userMatcher) Matches(x interface{}) bool {
    user, ok := x.(*User)
    if !ok {
        return false
    }
    return user.Email == m.expectedEmail
}

func (m userMatcher) String() string {
    return "matches user with email: " + m.expectedEmail
}

func UserWithEmail(email string) gomock.Matcher {
    return userMatcher{expectedEmail: email}
}

// Usage in test
func TestCustomMatcher(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockUserRepository(ctrl)

    mockRepo.EXPECT().
        Create(UserWithEmail("alice@example.com")).
        Return(nil)

    service := NewUserService(mockRepo)
    service.CreateUser(&User{Name: "Alice", Email: "alice@example.com"})
}
go
// 用于复杂验证的自定义匹配器
type userMatcher struct {
    expectedEmail string
}

func (m userMatcher) Matches(x interface{}) bool {
    user, ok := x.(*User)
    if !ok {
        return false
    }
    return user.Email == m.expectedEmail
}

func (m userMatcher) String() string {
    return "matches user with email: " + m.expectedEmail
}

func UserWithEmail(email string) gomock.Matcher {
    return userMatcher{expectedEmail: email}
}

// 在测试中使用
func TestCustomMatcher(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockUserRepository(ctrl)

    mockRepo.EXPECT().
        Create(UserWithEmail("alice@example.com")).
        Return(nil)

    service := NewUserService(mockRepo)
    service.CreateUser(&User{Name: "Alice", Email: "alice@example.com"})
}

Benchmark Testing

基准测试

Basic Benchmarks

基础基准测试

go
func BenchmarkAdd(b *testing.B) {
    calc := NewCalculator()

    for i := 0; i < b.N; i++ {
        calc.Add(2, 3)
    }
}

func BenchmarkStringConcatenation(b *testing.B) {
    b.Run("plus operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = "hello" + "world"
        }
    })

    b.Run("strings.Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            sb.WriteString("hello")
            sb.WriteString("world")
            _ = sb.String()
        }
    })
}
go
func BenchmarkAdd(b *testing.B) {
    calc := NewCalculator()

    for i := 0; i < b.N; i++ {
        calc.Add(2, 3)
    }
}

func BenchmarkStringConcatenation(b *testing.B) {
    b.Run("plus operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = "hello" + "world"
        }
    })

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

Running Benchmarks

运行基准测试

bash
undefined
bash
undefined

Run all benchmarks

运行所有基准测试

go test -bench=.
go test -bench=.

Run specific benchmark

运行指定基准测试

go test -bench=BenchmarkAdd
go test -bench=BenchmarkAdd

With memory allocation stats

包含内存分配统计

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

Compare benchmarks

对比基准测试结果

go test -bench=. -benchmem > old.txt
go test -bench=. -benchmem > old.txt

Make changes

进行代码修改

go test -bench=. -benchmem > new.txt benchstat old.txt new.txt
undefined
go test -bench=. -benchmem > new.txt benchstat old.txt new.txt
undefined

Benchmark Output Example

基准测试输出示例

BenchmarkAdd-8                  1000000000      0.25 ns/op      0 B/op      0 allocs/op
BenchmarkStringBuilder-8        50000000        28.5 ns/op      64 B/op     1 allocs/op
Reading:
50000000
iterations,
28.5 ns/op
per operation,
64 B/op
bytes allocated per op,
1 allocs/op
allocations per op
BenchmarkAdd-8                  1000000000      0.25 ns/op      0 B/op      0 allocs/op
BenchmarkStringBuilder-8        50000000        28.5 ns/op      64 B/op     1 allocs/op
输出解读:
50000000
次迭代,每次操作耗时
28.5 ns/op
,每次操作分配
64 B/op
内存,每次操作分配
1 allocs/op
次内存。

Advanced Testing Patterns

高级测试模式

httptest for HTTP Handlers

用于HTTP处理器的httptest

go
import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserHandler(t *testing.T) {
    handler := http.HandlerFunc(UserHandler)

    req := httptest.NewRequest("GET", "/users/1", nil)
    rec := httptest.NewRecorder()

    handler.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.Contains(t, rec.Body.String(), "Alice")
}

func TestHTTPClient(t *testing.T) {
    // Mock server
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Equal(t, "/api/users", r.URL.Path)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"id": 1, "name": "Alice"}`))
    }))
    defer server.Close()

    // Test client against mock server
    client := NewAPIClient(server.URL)
    user, err := client.GetUser(1)

    assert.NoError(t, err)
    assert.Equal(t, "Alice", user.Name)
}
go
import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestUserHandler(t *testing.T) {
    handler := http.HandlerFunc(UserHandler)

    req := httptest.NewRequest("GET", "/users/1", nil)
    rec := httptest.NewRecorder()

    handler.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.Contains(t, rec.Body.String(), "Alice")
}

func TestHTTPClient(t *testing.T) {
    // 模拟服务器
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Equal(t, "/api/users", r.URL.Path)
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"id": 1, "name": "Alice"}`))
    }))
    defer server.Close()

    // 针对模拟服务器测试客户端
    client := NewAPIClient(server.URL)
    user, err := client.GetUser(1)

    assert.NoError(t, err)
    assert.Equal(t, "Alice", user.Name)
}

Race Detector

竞态检测器

Detect data races in concurrent code:
bash
go test -race ./...
Example test for concurrent safety:
go
func TestConcurrentMapAccess(t *testing.T) {
    cache := NewSafeCache()

    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(val int) {
            defer wg.Done()
            cache.Set(fmt.Sprintf("key%d", val), val)
        }(i)
    }

    wg.Wait()
    assert.Equal(t, 100, cache.Len())
}
检测并发代码中的数据竞态:
bash
go test -race ./...
并发安全测试示例:
go
func TestConcurrentMapAccess(t *testing.T) {
    cache := NewSafeCache()

    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(val int) {
            defer wg.Done()
            cache.Set(fmt.Sprintf("key%d", val), val)
        }(i)
    }

    wg.Wait()
    assert.Equal(t, 100, cache.Len())
}

Golden File Testing

黄金文件测试

Test against expected output files:
go
func TestRenderTemplate(t *testing.T) {
    output := RenderTemplate("user", User{Name: "Alice"})

    goldenFile := "testdata/user_template.golden"

    if *update {
        // Update golden file: go test -update
        os.WriteFile(goldenFile, []byte(output), 0644)
    }

    expected, err := os.ReadFile(goldenFile)
    require.NoError(t, err)

    assert.Equal(t, string(expected), output)
}

var update = flag.Bool("update", false, "update golden files")
与预期输出文件进行对比测试:
go
func TestRenderTemplate(t *testing.T) {
    output := RenderTemplate("user", User{Name: "Alice"})

    goldenFile := "testdata/user_template.golden"

    if *update {
        // 更新黄金文件:执行go test -update
        os.WriteFile(goldenFile, []byte(output), 0644)
    }

    expected, err := os.ReadFile(goldenFile)
    require.NoError(t, err)

    assert.Equal(t, string(expected), output)
}

var update = flag.Bool("update", false, "更新黄金文件")

CI/CD Integration

CI/CD集成

GitHub Actions Example

GitHub Actions示例

yaml
undefined
yaml
undefined

.github/workflows/test.yml

.github/workflows/test.yml

name: Tests
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
  - name: Set up Go
    uses: actions/setup-go@v4
    with:
      go-version: '1.23'

  - name: Run tests
    run: go test -v -race -coverprofile=coverage.out ./...

  - name: Upload coverage
    uses: codecov/codecov-action@v3
    with:
      files: ./coverage.out

  - name: Check coverage threshold
    run: |
      go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
      awk '{if ($1 < 80) exit 1}'
undefined
name: Tests
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3
  - name: Set up Go
    uses: actions/setup-go@v4
    with:
      go-version: '1.23'

  - name: Run tests
    run: go test -v -race -coverprofile=coverage.out ./...

  - name: Upload coverage
    uses: codecov/codecov-action@v3
    with:
      files: ./coverage.out

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

Coverage Enforcement

覆盖率强制检查

bash
undefined
bash
undefined

Generate coverage report

生成覆盖率报告

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

View coverage in terminal

在终端查看覆盖率

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

Generate HTML report

生成HTML报告

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

Check coverage threshold (fail if < 80%)

检查覆盖率阈值(若低于80%则失败)

go test -coverprofile=coverage.out ./... &&
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'
undefined
go test -coverprofile=coverage.out ./... &&
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'
undefined

Decision Trees

决策树

When to Use Each Testing Tool

何时使用各测试工具

Use Standard
testing
Package When:
  • Simple unit tests with few assertions
  • No external dependencies to mock
  • Performance benchmarking
  • Minimal dependencies preferred
Use Testify When:
  • Need readable assertions (
    assert.Equal
    vs verbose checks)
  • Test suites with setup/teardown
  • Multiple similar test cases
  • Prefer expressive test code
Use Gomock When:
  • Testing code with interface dependencies
  • Need precise call verification (times, order)
  • Complex mock behavior with multiple scenarios
  • Type-safe mocking required
Use Benchmarks When:
  • Optimizing performance-critical code
  • Comparing algorithm implementations
  • Detecting performance regressions
  • Memory allocation profiling
Use httptest When:
  • Testing HTTP handlers
  • Mocking external HTTP APIs
  • Integration testing HTTP clients
  • Testing middleware chains
Use Race Detector When:
  • Writing concurrent code
  • Using goroutines and channels
  • Shared state across goroutines
  • CI/CD for all concurrent code
当以下情况时使用标准
testing
包:
  • 带有少量断言的简单单元测试
  • 无需模拟外部依赖
  • 性能基准测试
  • 偏好最小化依赖
当以下情况时使用Testify:
  • 需要可读性强的断言(
    assert.Equal
    vs 冗长的检查)
  • 带有初始化/清理步骤的测试套件
  • 多个相似的测试用例
  • 偏好表达性强的测试代码
当以下情况时使用Gomock:
  • 测试带有接口依赖的代码
  • 需要精确的调用验证(次数、顺序)
  • 带有多场景的复杂模拟行为
  • 需要类型安全的模拟
当以下情况时使用基准测试:
  • 优化性能关键代码
  • 对比算法实现
  • 检测性能回归
  • 内存分配分析
当以下情况时使用httptest:
  • 测试HTTP处理器
  • 模拟外部HTTP API
  • 集成测试HTTP客户端
  • 测试中间件链
当以下情况时使用竞态检测器:
  • 编写并发代码
  • 使用goroutine和通道
  • goroutine间共享状态
  • 对所有并发代码执行CI/CD检查

Anti-Patterns to Avoid

需避免的反模式

Don't Mock Everything
go
// WRONG: Over-mocking makes tests brittle
mockLogger := mocks.NewMockLogger(ctrl)
mockConfig := mocks.NewMockConfig(ctrl)
mockMetrics := mocks.NewMockMetrics(ctrl)
// Too many mocks = fragile test
Do: Mock Only External Dependencies
go
// CORRECT: Mock only database, use real logger/config
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo, realLogger, realConfig)
Don't Test Implementation Details
go
// WRONG: Testing internal state
assert.Equal(t, "processing", service.internalState)
Do: Test Public Behavior
go
// CORRECT: Test observable outcomes
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
Don't Ignore Error Cases
go
// WRONG: Only testing happy path
func TestGetUser(t *testing.T) {
    user, _ := service.GetUser(1) // Ignoring error!
    assert.NotNil(t, user)
}
Do: Test Error Conditions
go
// CORRECT: Test both success and error cases
func TestGetUser_NotFound(t *testing.T) {
    user, err := service.GetUser(999)
    assert.Error(t, err)
    assert.Nil(t, user)
    assert.Contains(t, err.Error(), "not found")
}
不要模拟所有依赖
go
// 错误:过度模拟会导致测试脆弱
mockLogger := mocks.NewMockLogger(ctrl)
mockConfig := mocks.NewMockConfig(ctrl)
mockMetrics := mocks.NewMockMetrics(ctrl)
// 过多模拟 = 脆弱的测试
正确做法:仅模拟外部依赖
go
// 正确:仅模拟数据库,使用真实的日志/配置
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo, realLogger, realConfig)
不要测试实现细节
go
// 错误:测试内部状态
assert.Equal(t, "processing", service.internalState)
正确做法:测试公开行为
go
// 正确:测试可观察的结果
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
不要忽略错误场景
go
// 错误:仅测试正常路径
func TestGetUser(t *testing.T) {
    user, _ := service.GetUser(1) // 忽略错误!
    assert.NotNil(t, user)
}
正确做法:测试错误场景
go
// 正确:同时测试成功和失败路径
func TestGetUser_NotFound(t *testing.T) {
    user, err := service.GetUser(999)
    assert.Error(t, err)
    assert.Nil(t, user)
    assert.Contains(t, err.Error(), "not found")
}

Best Practices

最佳实践

  1. Colocate Tests: Place
    *_test.go
    files alongside source code
  2. Use Subtests: Organize related tests with
    t.Run()
  3. Parallel When Safe: Enable
    t.Parallel()
    for independent tests
  4. Mock Interfaces: Design for testability with interface dependencies
  5. Test Errors: Verify both success and failure paths
  6. Benchmark Critical Paths: Profile performance-sensitive code
  7. Run Race Detector: Always use
    -race
    for concurrent code
  8. Enforce Coverage: Set minimum thresholds in CI (typically 80%)
  9. Use Golden Files: Test complex outputs with expected files
  10. Keep Tests Fast: Mock slow operations, use
    -short
    flag for quick runs
  1. 测试文件同目录放置: 将
    *_test.go
    文件与源代码放在一起
  2. 使用子测试: 使用
    t.Run()
    组织相关测试
  3. 安全时启用并行: 为独立测试启用
    t.Parallel()
  4. 模拟接口: 基于接口设计以提升可测试性
  5. 测试错误: 验证成功和失败路径
  6. 基准测试关键路径: 分析性能敏感代码
  7. 运行竞态检测器: 对并发代码始终使用
    -race
    参数
  8. 强制覆盖率: 在CI中设置最低阈值(通常为80%)
  9. 使用黄金文件: 对复杂输出使用预期文件测试
  10. 保持测试快速: 模拟慢速操作,使用
    -short
    参数快速运行

Resources

资源

Official Documentation:
Testing Frameworks:
Recent Guides (2025):
Related Skills:
  • golang-engineer: Core Go patterns and concurrency
  • verification-before-completion: Testing as part of "done"
  • testing-anti-patterns: Avoid common testing mistakes
官方文档:
测试框架:
最新指南(2025年):
相关技能:
  • golang-engineer: 核心Go模式与并发
  • verification-before-completion: 作为“完成”标准的测试
  • testing-anti-patterns: 避免常见测试错误

Quick Reference

快速参考

Run Tests

运行测试

bash
go test ./...                    # All tests
go test -v ./...                 # Verbose output
go test -short ./...             # Skip slow tests
go test -run TestUserCreate      # Specific test
go test -race ./...              # With race detector
go test -cover ./...             # With coverage
go test -coverprofile=c.out ./... # Coverage file
go test -bench=. -benchmem       # Benchmarks with memory
bash
go test ./...                    # 所有测试
go test -v ./...                 # 详细输出
go test -short ./...             # 跳过慢速测试
go test -run TestUserCreate      # 指定测试
go test -race ./...              # 启用竞态检测器
go test -cover ./...             # 启用覆盖率统计
go test -coverprofile=c.out ./... # 生成覆盖率文件
go test -bench=. -benchmem       # 带内存统计的基准测试

Generate Mocks

生成模拟对象

bash
go generate ./...                           # All //go:generate directives
mockgen -source=interface.go -destination=mock.go
bash
go generate ./...                           # 所有//go:generate指令
mockgen -source=interface.go -destination=mock.go

Coverage Analysis

覆盖率分析

bash
go tool cover -func=coverage.out           # Coverage per function
go tool cover -html=coverage.out           # HTML report

Token Estimate: ~4,500 tokens (entry point + full content) Version: 1.0.0 Last Updated: 2025-12-03
bash
go tool cover -func=coverage.out           # 按函数查看覆盖率
go tool cover -html=coverage.out           # 生成HTML报告

Token估算: ~4,500个Token(入口点+完整内容) 版本: 1.0.0 最后更新: 2025-12-03