关于 golang-testing
面向 Go 开发者的测试技能,涵盖单元测试、集成测试、基准测试和测试组织。内容包括表驱动测试、基于接口的 Mock、测试夹具及 HTTP 处理器测试。
主要功能
- 01表驱动测试模式
- 02基于接口的 Mock
- 03Testify 断言
- 04使用 Testcontainers 进行集成测试
- 05测试夹具与设置清理
- 06基准测试与覆盖率
适用场景
- 01编写 Go 单元测试并覆盖边界情况
- 02使用容器测试数据库集成
- 03为性能关键代码进行基准测试
- 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 ./...
最佳实践
- 测试行为,而非实现 - 聚焦于输入和输出
- 每项测试只做一个断言 - 让测试保持聚焦且清晰
- 使用 t.Helper() - 标记辅助函数,以便更好地报告错误
- 并行测试 - 对相互独立的测试使用
t.Parallel() - 使用描述性名称 -
TestUserService_CreateUser_WithInvalidEmail - 测试边界情况 - 空输入、nil 值、边界条件
- 保持测试快速 - 使用模拟对象,通过
-short跳过耗时测试 - 避免测试污染 - 每项测试都应相互独立
评论
支持 Markdown