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