Go语言测试技能指南Skill golang-testing

本技能提供Go语言应用程序的全面测试策略指导,涵盖单元测试、集成测试、基准测试和测试组织。内容包括表驱动测试、接口模拟、Testcontainers集成测试、Testify断言库、HTTP处理器测试等核心模式。适用于Go开发者提升代码质量和测试效率,关键词:Go测试、单元测试、集成测试、基准测试、表驱动测试、模拟测试、Testify、Testcontainers、Go语言开发、软件测试。

测试 0 次安装 0 次浏览 更新于 2/28/2026

name: golang-testing description: 全面的Go测试模式,包括表驱动测试、模拟、集成测试、基准测试和测试组织。 author: Joseph OBrien status: unpublished updated: ‘2025-12-23’ version: 1.0.1 tag: skill type: skill

Go语言测试

本技能为Go应用程序提供全面的测试策略指导,包括单元测试、集成测试、基准测试和测试组织。

何时使用此技能

  • 为Go代码编写单元测试时
  • 创建表驱动测试时
  • 使用接口模拟依赖项时
  • 使用测试容器编写集成测试时
  • 对性能关键代码进行基准测试时
  • 组织测试套件和测试夹具时

表驱动测试

基本模式

func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"正数", 2, 3, 5},
        {"负数", -2, -3, -5},
        {"混合数", -2, 3, 1},
        {"零值", 0, 0, 0},
    }

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

包含错误案例

func TestDivide(t *testing.T) {
    tests := []struct {
        name      string
        a, b      int
        expected  int
        wantErr   bool
        errString string
    }{
        {"有效除法", 10, 2, 5, false, ""},
        {"除以零", 10, 0, 0, true, "除以零"},
        {"负结果", -10, 2, -5, false, ""},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Divide(tt.a, tt.b)

            if tt.wantErr {
                if err == nil {
                    t.Fatalf("期望错误,但得到nil")
                }
                if !strings.Contains(err.Error(), tt.errString) {
                    t.Errorf("错误 = %v; 期望包含 %q", err, tt.errString)
                }
                return
            }

            if err != nil {
                t.Fatalf("意外错误: %v", err)
            }
            if result != tt.expected {
                t.Errorf("Divide(%d, %d) = %d; 期望 %d", tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

基于接口的模拟

定义接口

// repository.go
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    Save(ctx context.Context, user *User) error
}

type EmailSender interface {
    Send(ctx context.Context, to, subject, body string) error
}

创建模拟实现

// mocks/user_repository.go
type MockUserRepository struct {
    FindByIDFunc func(ctx context.Context, id string) (*User, error)
    SaveFunc     func(ctx context.Context, user *User) error
}

func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*User, error) {
    if m.FindByIDFunc != nil {
        return m.FindByIDFunc(ctx, id)
    }
    return nil, nil
}

func (m *MockUserRepository) Save(ctx context.Context, user *User) error {
    if m.SaveFunc != nil {
        return m.SaveFunc(ctx, user)
    }
    return nil
}

在测试中使用

func TestUserService_GetUser(t *testing.T) {
    expectedUser := &User{ID: "123", Name: "John"}

    repo := &MockUserRepository{
        FindByIDFunc: func(ctx context.Context, id string) (*User, error) {
            if id == "123" {
                return expectedUser, nil
            }
            return nil, ErrNotFound
        },
    }

    service := NewUserService(repo)

    t.Run("存在的用户", func(t *testing.T) {
        user, err := service.GetUser(context.Background(), "123")
        if err != nil {
            t.Fatalf("意外错误: %v", err)
        }
        if user.Name != expectedUser.Name {
            t.Errorf("得到名称 %q; 期望 %q", user.Name, expectedUser.Name)
        }
    })

    t.Run("不存在的用户", func(t *testing.T) {
        _, err := service.GetUser(context.Background(), "456")
        if !errors.Is(err, ErrNotFound) {
            t.Errorf("得到错误 %v; 期望 ErrNotFound", err)
        }
    })
}

Testify断言库

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestWithTestify(t *testing.T) {
    // assert在失败时继续执行
    assert.Equal(t, 5, Add(2, 3), "加法应该有效")
    assert.NotNil(t, result)
    assert.Len(t, items, 3)
    assert.Contains(t, slice, item)
    assert.True(t, condition)
    assert.NoError(t, err)
    assert.ErrorIs(t, err, ErrNotFound)

    // require在失败时停止测试
    require.NoError(t, err, "设置必须成功")
    require.NotNil(t, config)
}

使用Testcontainers的集成测试

import (
    "context"
    "testing"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
)

func TestUserRepository_Integration(t *testing.T) {
    if testing.Short() {
        t.Skip("在短模式下跳过集成测试")
    }

    ctx := context.Background()

    // 启动PostgreSQL容器
    pgContainer, err := postgres.Run(ctx,
        "postgres:15-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    require.NoError(t, err)
    defer pgContainer.Terminate(ctx)

    // 获取连接字符串
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    // 连接并运行迁移
    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)
    defer db.Close()

    runMigrations(db)

    // 创建存储库并测试
    repo := NewUserRepository(db)

    t.Run("保存并查找用户", func(t *testing.T) {
        user := &User{ID: "123", Name: "John", Email: "john@example.com"}

        err := repo.Save(ctx, user)
        require.NoError(t, err)

        found, err := repo.FindByID(ctx, "123")
        require.NoError(t, err)
        assert.Equal(t, user.Name, found.Name)
    })
}

测试夹具

设置/清理模式

func TestMain(m *testing.M) {
    // 全局设置
    setup()

    code := m.Run()

    // 全局清理
    teardown()

    os.Exit(code)
}

func setup() {
    // 初始化测试数据库、加载夹具等
}

func teardown() {
    // 清理资源
}

每个测试的设置

func setupTest(t *testing.T) (*UserService, func()) {
    t.Helper()

    db := setupTestDB(t)
    repo := NewUserRepository(db)
    service := NewUserService(repo)

    cleanup := func() {
        db.Close()
    }

    return service, cleanup
}

func TestUserService(t *testing.T) {
    service, cleanup := setupTest(t)
    defer cleanup()

    // 使用服务运行测试
}

基准测试

func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}

func BenchmarkFibonacciParallel(b *testing.B) {
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Fibonacci(20)
        }
    })
}

// 使用子基准测试
func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000}

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

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

测试HTTP处理器

func TestHandler_GetUser(t *testing.T) {
    // 设置模拟服务
    service := &MockUserService{
        GetUserFunc: func(ctx context.Context, id string) (*User, error) {
            return &User{ID: id, Name: "John"}, nil
        },
    }

    handler := NewHandler(service)

    t.Run("成功", func(t *testing.T) {
        req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
        rec := httptest.NewRecorder()

        handler.GetUser(rec, req)

        assert.Equal(t, http.StatusOK, rec.Code)

        var response User
        err := json.NewDecoder(rec.Body).Decode(&response)
        require.NoError(t, err)
        assert.Equal(t, "John", response.Name)
    })

    t.Run("未找到", func(t *testing.T) {
        service.GetUserFunc = func(ctx context.Context, id string) (*User, error) {
            return nil, ErrNotFound
        }

        req := httptest.NewRequest(http.MethodGet, "/users/999", nil)
        rec := httptest.NewRecorder()

        handler.GetUser(rec, req)

        assert.Equal(t, http.StatusNotFound, rec.Code)
    })
}

测试组织

文件结构

/internal
  /user
    user.go
    user_test.go          # 单元测试
    user_integration_test.go  # 集成测试(构建标签)
    testdata/             # 测试夹具
      users.json

集成测试的构建标签

//go:build integration

package user

func TestIntegration(t *testing.T) {
    // 集成测试代码
}

运行命令:go test -tags=integration ./...

覆盖率

# 生成覆盖率
go test -coverprofile=coverage.out ./...

# 在浏览器中查看
go tool cover -html=coverage.out

# 检查覆盖率百分比
go test -cover ./...

最佳实践

  1. 测试行为,而非实现 - 关注输入和输出
  2. 每个测试一个断言 - 保持测试专注和清晰
  3. 使用t.Helper() - 标记辅助函数以获得更好的错误报告
  4. 并行测试 - 对独立测试使用t.Parallel()
  5. 描述性名称 - TestUserService_CreateUser_WithInvalidEmail
  6. 测试边界情况 - 空输入、nil值、边界条件
  7. 保持测试快速 - 使用模拟,使用-short跳过慢速测试
  8. 避免测试污染 - 每个测试应该是独立的