mcpField
G
golang-testing

golang-testing

github.com/89jobrien测试与质量442 次安装9 周前更新
代码仓库

关于 golang-testing

面向 Go 开发者的测试技能,涵盖单元测试、集成测试、基准测试和测试组织。内容包括表驱动测试、基于接口的 Mock、测试夹具及 HTTP 处理器测试。

主要功能

  1. 01表驱动测试模式
  2. 02基于接口的 Mock
  3. 03Testify 断言
  4. 04使用 Testcontainers 进行集成测试
  5. 05测试夹具与设置清理
  6. 06基准测试与覆盖率

适用场景

  1. 01编写 Go 单元测试并覆盖边界情况
  2. 02使用容器测试数据库集成
  3. 03为性能关键代码进行基准测试
  4. 04组织测试套件和测试夹具

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/89jobrien/steve --skill golang-testing

安装后在 Claude Code 中输入/golang-testing确认已加载。

详细介绍

Go 测试

本技能介绍 Go 应用的全面测试策略,包括单元测试、集成测试、基准测试和测试组织。

何时使用此技能

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

表驱动测试

基本模式
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"mixed numbers", -2, 3, 1},
        {"zeros", 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; want %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
    }{
        {"valid division", 10, 2, 5, false, ""},
        {"divide by zero", 10, 0, 0, true, "division by zero"},
        {"negative result", -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("expected error, got nil")
                }
                if !strings.Contains(err.Error(), tt.errString) {
                    t.Errorf("error = %v; want containing %q", err, tt.errString)
                }
                return
            }

            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if result != tt.expected {
                t.Errorf("Divide(%d, %d) = %d; want %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("existing user", func(t *testing.T) {
        user, err := service.GetUser(context.Background(), "123")
        if err != nil {
            t.Fatalf("unexpected error: %v", err)
        }
        if user.Name != expectedUser.Name {
            t.Errorf("got name %q; want %q", user.Name, expectedUser.Name)
        }
    })

    t.Run("non-existing user", func(t *testing.T) {
        _, err := service.GetUser(context.Background(), "456")
        if !errors.Is(err, ErrNotFound) {
            t.Errorf("got error %v; want ErrNotFound", err)
        }
    })
}

Testify 断言

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

func TestWithTestify(t *testing.T) {
    // assert continues on failure
    assert.Equal(t, 5, Add(2, 3), "addition should work")
    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 stops test on failure
    require.NoError(t, err, "setup must succeed")
    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("skipping integration test in short mode")
    }

    ctx := context.Background()

    // Start PostgreSQL container
    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)

    // Get connection string
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)

    // Connect and run migrations
    db, err := sql.Open("postgres", connStr)
    require.NoError(t, err)
    defer db.Close()

    runMigrations(db)

    // Create repository and test
    repo := NewUserRepository(db)

    t.Run("save and find user", func(t *testing.T) {
        user := &User{ID: "123", Name: "John", Email: "[email protected]"}

        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) {
    // Global setup
    setup()

    code := m.Run()

    // Global teardown
    teardown()

    os.Exit(code)
}

func setup() {
    // Initialize test database, load fixtures, etc.
}

func teardown() {
    // Clean up resources
}
每项测试的设置
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()

    // Run tests using service
}

基准测试

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

// With sub-benchmarks
func BenchmarkSort(b *testing.B) {
    sizes := []int{100, 1000, 10000}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("size-%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) {
    // Setup mock service
    service := &MockUserService{
        GetUserFunc: func(ctx context.Context, id string) (*User, error) {
            return &User{ID: id, Name: "John"}, nil
        },
    }

    handler := NewHandler(service)

    t.Run("success", 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("not found", 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          # Unit tests
    user_integration_test.go  # Integration tests (build tag)
    testdata/             # Test fixtures
      users.json
集成测试的构建标签
//go:build integration

package user

func TestIntegration(t *testing.T) {
    // Integration test code
}

使用以下命令运行:go test -tags=integration ./...

覆盖率

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

# View in browser
go tool cover -html=coverage.out

# Check coverage percentage
go test -cover ./...

最佳实践

  1. 测试行为,而非实现 - 聚焦于输入和输出
  2. 每项测试只做一个断言 - 让测试保持聚焦且清晰
  3. 使用 t.Helper() - 标记辅助函数,以便更好地报告错误
  4. 并行测试 - 对相互独立的测试使用 t.Parallel()
  5. 使用描述性名称 - TestUserService_CreateUser_WithInvalidEmail
  6. 测试边界情况 - 空输入、nil 值、边界条件
  7. 保持测试快速 - 使用模拟对象,通过 -short 跳过耗时测试
  8. 避免测试污染 - 每项测试都应相互独立

评论

支持 Markdown