Go 测试进阶:gomock、testcontainers 与 Fuzzing
目录
前言
测试是后端工程质量的生命线。2022 年的 Go 生态中,三个测试工具/特性格外引人注目:gomock(接口 mock 的最佳实践)、testcontainers(真正的集成测试)、原生 Fuzzing(Go 1.18 里程碑特性)。
一、gomock:优雅的接口 Mock
安装与生成
go install github.com/golang/mock/mockgen@v1.6.0# 为 UserRepository 接口生成 mock
mockgen -source=repository.go -destination=mock/mock_repository.go -package=mock定义接口
// repository.go
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
Save(ctx context.Context, user *User) error
List(ctx context.Context, page, size int) ([]User, int64, error)
}在测试中使用 Mock
// service_test.go
func TestUserService_GetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockUserRepository(ctrl)
svc := NewUserService(mockRepo)
expectedUser := &User{ID: 1, Name: "张三"}
// 设置期望调用
mockRepo.EXPECT().
GetByID(gomock.Any(), int64(1)).
Return(expectedUser, nil).
Times(1)
user, err := svc.GetUser(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, "张三", user.Name)
}常用匹配器
// 任意参数
mockRepo.EXPECT().GetByID(gomock.Any(), gomock.Any())
// 特定条件
mockRepo.EXPECT().GetByID(gomock.Any(), gomock.Eq(int64(100)))
// 自定义匹配
mockRepo.EXPECT().Save(gomock.Any(), gomock.AssignableToTypeOf(&User{}))
// 调用顺序
gomock.InOrder(
mockRepo.EXPECT().GetByID(...),
mockRepo.EXPECT().Save(...),
)二、testcontainers:真正的集成测试
testcontainers-go 让你在测试中启动真实的 Docker 容器(MySQL、Redis、Kafka 等),用完自动销毁。
安装
go get github.com/testcontainers/testcontainers-go
go get github.com/testcontainers/testcontainers-go/modules/mysqlMySQL 集成测试
func TestUserRepository_MySQL(t *testing.T) {
ctx := context.Background()
// 启动 MySQL 容器
mysql, err := mysql.RunContainer(ctx,
testcontainers.WithImage("mysql:8.0"),
mysql.WithDatabase("testdb"),
mysql.WithUsername("root"),
mysql.WithPassword("password"),
)
require.NoError(t, err)
defer mysql.Terminate(ctx)
// 获取连接地址
host, err := mysql.Host(ctx)
port, err := mysql.MappedPort(ctx, "3306/tcp")
dsn := fmt.Sprintf("root:password@tcp(%s:%s)/testdb?charset=utf8mb4", host, port.Port())
db, err := sql.Open("mysql", dsn)
require.NoError(t, err)
// 执行测试
repo := NewUserRepository(db)
users, err := repo.List(ctx, 1, 10)
assert.NoError(t, err)
t.Logf("查询到 %d 个用户", len(users))
}Redis 集成测试
func TestRedisCache(t *testing.T) {
ctx := context.Background()
redis, err := redis.RunContainer(ctx,
testcontainers.WithImage("redis:7-alpine"),
)
require.NoError(t, err)
defer redis.Terminate(ctx)
addr, _ := redis.Endpoint(ctx, "redis://")
client := redisClient.NewClient(&redisClient.Options{Addr: addr})
err = client.Set(ctx, "key", "value", time.Hour).Err()
assert.NoError(t, err)
val, _ := client.Get(ctx, "key").Result()
assert.Equal(t, "value", val)
}最佳实践:testcontainers 很适合 CI 环境,GitHub Actions 中只要 Docker 可用就能跑。本地开发建议配合
-short标记区分单元测试和集成测试。
三、Go 原生 Fuzzing(Go 1.18)
Fuzzing(模糊测试)是 Go 1.18 引入的原生特性,它能自动生成随机输入来测试代码,发现边界条件和安全漏洞。
基础用法
// 需要测试的函数
func ParsePhone(phone string) (bool, error) {
if len(phone) != 11 {
return false, fmt.Errorf("invalid length")
}
for _, c := range phone {
if c < '0' || c > '9' {
return false, fmt.Errorf("invalid char: %c", c)
}
}
return true, nil
}
// Fuzz 测试函数(必须以 FuzzXxx 命名)
func FuzzParsePhone(f *testing.F) {
// 种子语料库
f.Add("13800138000")
f.Add("12345678901")
f.Add("hello")
f.Fuzz(func(t *testing.T, phone string) {
result, err := ParsePhone(phone)
if len(phone) == 11 && isAllDigit(phone) {
assert.True(t, result)
assert.NoError(t, err)
}
})
}运行 Fuzz 测试:
go test -fuzz=FuzzParsePhone -fuzztime=30s真实案例:JSON 解析边界测试
func FuzzJSONUnmarshal(f *testing.F) {
testcases := []string{
`{"name":"test"}`,
`{"age": 25}`,
`{"data": null}`,
}
for _, tc := range testcases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, input string) {
var data map[string]interface{}
err := json.Unmarshal([]byte(input), &data)
// 有错误是正常的,但不能 panic
if err != nil {
return
}
// 验证反序列化的结果
for k, v := range data {
assert.NotEmpty(t, k)
switch val := v.(type) {
case float64:
assert.False(t, math.IsNaN(val))
}
}
})
}HTTP 处理器 Fuzzing
func FuzzUserHandler(f *testing.F) {
f.Add("id=1")
f.Add("id=abc")
f.Add("name=张三&age=25")
f.Fuzz(func(t *testing.T, query string) {
req := httptest.NewRequest("GET", "/user?"+query, nil)
w := httptest.NewRecorder()
// Handler 内部不能 panic
assert.NotPanics(t, func() {
UserHandler(w, req)
})
})
}构建一个完整的测试金字塔
// 从下往上:单元测试 → 集成测试 → Fuzzing
func TestUserService(t *testing.T) {
if testing.Short() {
t.Skip("跳过集成测试")
}
t.Run("Unit-Mock", func(t *testing.T) {
// 用 gomock 测试 service 逻辑
})
t.Run("Integration-MySQL", func(t *testing.T) {
// 用 testcontainers 测试数据库交互
})
t.Run("Fuzz-Parse", func(t *testing.T) {
// 用 Fuzzing 测试输入边界
})
}# 只跑单元测试
go test -short ./...
# 跑所有测试(含集成)
go test ./...
# Fuzzing 30 秒
go test -fuzz=./... -fuzztime=30s总结
2022 年的 Go 测试生态已经足够成熟:
| 工具/特性 | 用途 | 推荐度 |
|---|---|---|
| gomock | 接口 Mock,隔离依赖 | ⭐⭐⭐⭐⭐ |
| testcontainers | 真实容器集成测试 | ⭐⭐⭐⭐ |
| 原生 Fuzzing | 边界发现、安全测试 | ⭐⭐⭐⭐⭐ |
写好测试的核心理念:单元测试保证逻辑正确,集成测试保证交互可靠,Fuzzing 保证边界安全,三者缺一不可。