# GoMock 与 GoMonkey 使用与对比详解(含最佳实践与常见坑)
本文详细介绍 Go 语言中两个常用的单元测试工具 GoMock 与 GoMonkey 的使用方法,并将它们的内容分块对比,最后给出总结建议。
# 1. GoMock
# 1.1 基本概念
- 定位:Go 官方推荐的 Mock 框架,由 Google 维护。
- 原理:通过接口(interface)生成 Mock 类型,在测试时替换真实实现。
- 特点:
# 1.2 安装与集成
| go install github.com/golang/mock/mockgen@latest |
推荐使用 go:generate 集成代码生成,避免手动执行:
# 1.3 使用示例
# 定义接口
| |
| package mypkg |
| |
| type DB interface { |
| GetUser(id int) (string, error) |
| } |
# 生成 Mock
| mockgen -source=db.go -destination=mock_db.go -package=mypkg |
# 编写测试
| package mypkg |
| |
| import ( |
| "testing" |
| |
| "github.com/golang/mock/gomock" |
| ) |
| |
| func TestGetUser(t *testing.T) { |
| ctrl := gomock.NewController(t) |
| defer ctrl.Finish() |
| |
| mockDB := NewMockDB(ctrl) |
| mockDB.EXPECT().GetUser(1).Return("Tom", nil) |
| |
| name, err := mockDB.GetUser(1) |
| if err != nil || name != "Tom" { |
| t.Fatalf("expected Tom, got %v, err: %v", name, err) |
| } |
| } |
# 匹配器与调用次数
| mockDB.EXPECT().GetUser(gomock.Eq(1)).Return("Tom", nil).Times(1) |
| mockDB.EXPECT().GetUser(gomock.AssignableToTypeOf(0)).AnyTimes() |
# 调用顺序控制
| gomock.InOrder( |
| mockDB.EXPECT().GetUser(1).Return("Tom", nil), |
| mockDB.EXPECT().GetUser(2).Return("Jerry", nil), |
| ) |
# 自定义行为(Do / DoAndReturn)
| mockDB.EXPECT().GetUser(gomock.Any()).DoAndReturn(func(id int) (string, error) { |
| if id <= 0 { |
| return "", fmt.Errorf("invalid id") |
| } |
| return fmt.Sprintf("user-%d", id), nil |
| }) |
# 1.4 优缺点
优点
- 类型安全,编译期检查
- 可维护性强
- 官方推荐,生态成熟
缺点
- 仅支持接口
- 需要额外的代码生成步骤
- 要求设计上支持依赖注入
# 1.5 常见坑与规避
- 使用次数约束时,漏写
Times/AnyTimes 导致默认期望一次,额外调用会失败。
- 期望未触发:测试结束时
Finish() 会校验未满足的期望,注意不要过度设置用不到的 EXPECT。
- 参数匹配不精确:优先用
gomock.Eq/AssignableToTypeOf ,少用 gomock.Any() 。
- 复杂顺序依赖:务必使用
gomock.InOrder ,否则仅凭次数无法保证顺序。
# 2. GoMonkey
# 2.1 基本概念
- 定位:基于 Monkey Patching 思路的 Go 测试工具。
- 原理:使用
reflect + unsafe + 汇编指令在运行时替换函数或方法的实现。
- 特点:
- 可以替换几乎任何函数、方法或变量
- 不需要改动原代码结构
- 属于运行时 Hack,非官方支持
# 2.2 安装
| go get github.com/agiledragon/gomonkey/v2 |
# 2.3 使用示例
# 替换全局函数
| package mypkg |
| |
| import ( |
| "testing" |
| "time" |
| "reflect" |
| |
| "github.com/agiledragon/gomonkey/v2" |
| "github.com/stretchr/testify/assert" |
| ) |
| |
| func TestPatchTimeNow(t *testing.T) { |
| patches := gomonkey.ApplyFunc(time.Now, func() time.Time { |
| return time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) |
| }) |
| defer patches.Reset() |
| |
| assert.Equal(t, 2020, time.Now().Year()) |
| } |
# 替换方法
| type User struct{} |
| |
| func (u *User) GetName() string { |
| return "RealName" |
| } |
| |
| func TestPatchMethod(t *testing.T) { |
| u := &User{} |
| patches := gomonkey.ApplyMethod(reflect.TypeOf(u), "GetName", func(_ *User) string { |
| return "MockName" |
| }) |
| defer patches.Reset() |
| |
| if name := u.GetName(); name != "MockName" { |
| t.Fatalf("expected MockName, got %v", name) |
| } |
| } |
# 序列化返回(多次调用不同返回)
| seq := []gomonkey.OutputCell<!--swig0-->, {Values: gomonkey.Params{"B"}}} |
| patches := gomonkey.ApplyFuncSeq(os.Getenv, seq) |
| defer patches.Reset() |
| |
| _ = os.Getenv("X") |
| _ = os.Getenv("X") |
# 2.4 优缺点
优点
- 灵活,几乎可以 Mock 一切
- 无需改动原有代码结构
- 对遗留系统、外部 SDK 友好
缺点
- 无类型检查,易引入隐藏错误
- 运行时 Hack,Go 升级可能失效
- 可读性和维护性差
# 2.5 常见坑与规避
- 函数内联导致补丁未生效:为确保可替换,测试时可加
-gcflags=all=-l 关闭内联。
- 与
-race 在部分版本 / 平台存在兼容性问题:若失败,尝试关闭 race 或锁定 Go / 库版本。
- 架构相关实现(asm)在少数 CPU/OS 组合失效:优先在 amd64 测试,关注仓库 release/issue。
- 忘记
Reset() 导致补丁泄漏影响其他用例:统一用 defer patches.Reset() 。
# 3. 总结与对比
| 对比项 |
GoMock |
GoMonkey |
| 原理 |
基于接口生成 Mock 类型 |
运行时替换函数指针 |
| Mock 范围 |
接口(interface) |
几乎任何函数、方法、变量 |
| 类型安全 |
是 |
否 |
| 维护性 |
高 |
低 |
| 侵入性 |
要求接口设计 |
无需改动代码 |
| 性能影响 |
极低 |
有一定运行时开销 |
| 稳定性 |
高,版本兼容好 |
较低,受 Go / 平台影响 |
| 并发 / 竞态 |
友好 |
与 -race 存在风险 |
| 适用场景 |
可控代码结构、接口依赖 |
遗留代码、第三方库、时间 / 随机数 / 环境变量 |
# 最佳实践建议
- 新项目 / 可控结构:优先 GoMock,保证类型安全和长期维护性。
- 遗留项目 / 第三方库:可用 GoMonkey,减少重构成本。
- 组合使用:
- 业务逻辑接口 → GoMock
- 全局函数、时间、环境变量、随机数 → GoMonkey
# 组合示例(接口用 GoMock,全局函数用 GoMonkey)
| |
| func TestSvc_ListUsers(t *testing.T) { |
| ctrl := gomock.NewController(t) |
| defer ctrl.Finish() |
| |
| repo := NewMockUserRepo(ctrl) |
| repo.EXPECT().List(gomock.Any()).Return([]User<!--swig1-->, nil).Times(1) |
| |
| patches := gomonkey.ApplyFunc(time.Now, func() time.Time { |
| return time.Date(2024, 10, 1, 0, 0, 0, 0, time.UTC) |
| }) |
| defer patches.Reset() |
| |
| svc := NewSvc(repo) |
| users, err := svc.ListUsers(context.Background()) |
| require.NoError(t, err) |
| require.Len(t, users, 1) |
| |
| } |
# 4. 参考链接
- GoMock GitHub
- mockgen 使用说明(README)
- gomock API(pkg.go.dev)
- GoMonkey GitHub
- GoMonkey 文档与示例