# GoMock 与 GoMonkey 使用与对比详解(含最佳实践与常见坑)

本文详细介绍 Go 语言中两个常用的单元测试工具 GoMockGoMonkey 的使用方法,并将它们的内容分块对比,最后给出总结建议。


# 1. GoMock

# 1.1 基本概念

  • 定位:Go 官方推荐的 Mock 框架,由 Google 维护。
  • 原理:通过接口(interface)生成 Mock 类型,在测试时替换真实实现。
  • 特点
    • 编译期类型安全
    • 依赖注入(DI)友好
    • 高可维护性

# 1.2 安装与集成

go install github.com/golang/mock/mockgen@latest

推荐使用 go:generate 集成代码生成,避免手动执行:

//go:generate mockgen -source=db.go -destination=mock_db.go -package=mypkg

# 1.3 使用示例

# 定义接口

// db.go
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") // "A"
_ = os.Getenv("X") // "B"

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

// 业务接口用 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)
    // 此处 svc 内部若使用 time.Now () 计算缓存键 / 日志,结果可被稳定断言
}

# 4. 参考链接

  • GoMock GitHub
  • mockgen 使用说明(README)
  • gomock API(pkg.go.dev)
  • GoMonkey GitHub
  • GoMonkey 文档与示例
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

ZJM 微信支付

微信支付

ZJM 支付宝

支付宝