文章目录
1.简介
gomonkey 是一个使单元测试中 Monkey Patching 变得简单的库,Monkey Patching 的核心思想来自 Bouke,你可以阅读 this blogpost 来了解其工作原理。
2.何为 Monkey Patch?
Monkey Patch 又叫猴子补丁,是一种在运行时动态修改或扩展现有类、模块或函数的技术。这种技术通常用于在不修改原始代码的情况下更改其行为或添加新功能。
这在调试、测试或临时修复问题时非常有用。
所以,利用 Monkey Patch 可以在单元测试中,在运行时改变代码的行为。
为什么叫 Monkey 呢,而不是其他动物?
Monkey Patch 的叫法并无权威的出处,其起源较为模糊,主要是在编程社区和技术文献中逐渐普及开来。
目前有两种来源说法:
- 这个词原来为 Guerrilla Patch,杂牌军、游击队,说明这部分不是原装的,在英文里 Guerilla 发音和 Gorllia(猩猩)相似,再后来就写了 Monkey。
- 还有一种解释是说由于这种方式将原来的代码弄乱了(messing with it),在英文里叫 monkeying about(顽皮的),所以叫 Monkey Patch。
3.作用
gomonkey 是一个用于 Go 语言的单元测试库,主要用于打桩(stubbing)和补丁(patching),它能够让开发者轻松地修改和替换代码中的某些行为,在不改动代码的前提下测试不同的情况。
注意,mock 不是 stub,可以参考 Mocks Aren’t Stubs。
什么时候需要使用 gomonkey 呢?
单元测试中,对一些不想执行的函数,比如有网络 IO 或对 DB 有写入的函数,因为测试环境网络不通或不想执行单测而向 DB 写入数据。
这种情况下,可以通过打桩(Stub)在测试中将函数替换为自定义实现或模拟值,用于控制函数的返回结果,使测试能够在不依赖实际代码逻辑的情况下运行。执行单测的时候会调用这个替代函数,相当于替代函数模拟了原函数。
gomonkey 功能强大,提供如下能力:
- 支持函数的补丁
- 支持公共成员方法的补丁
- 支持私有成员方法的补丁
- 支持接口的补丁
- 支持函数变量的补丁
- 支持全局变量的补丁
- 支持对函数的指定顺序补丁
- 支持对成员方法的指定顺序补丁
- 支持对接口的指定顺序补丁
- 支持对函数变量的指定顺序补丁
gomonkey 是基于 Go 语言的反射机制实现的,因此它的用法可能会违反类型安全,并且对代码的维护性有负面影响。因此,在使用 gomonkey 时应该谨慎,并且主要限于测试环境中。
4.注意
gomonkey 是一个 Go 单元测试中十分常用的打桩工具,它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。
gomonkey 很强大,但是使用时需注意以下事项:
- gomonkey 不支持内联函数,在测试的时候需要通过命令行参数
-gcflags=-l
(Go 1.10 以下)或-gcflags=all=-l
(Go 1.10 及以上)关闭 Go 语言的内联优化。 - gomonkey 不是线程安全的,所以不要把它用到并发的单元测试中。
5.安装
低于 v2.1.0,例如 v2.0.2。
$ go get github.com/agiledragon/gomonkey@v2.0.2
V2.1.0 及以上版本,例如 v2.11.0。
$ go get github.com/agiledragon/gomonkey/v2@v2.11.0
6.示例
6.1 为函数打桩
利用以下两个函数可以实现为函数打桩。
func ApplyFunc(target, double interface{}) *Patches
func ApplyFuncReturn(target interface{}, output ...interface{}) *Patches
ApplyFunc
ApplyFunc 将目标函数 target 替换为自定义实现 double。
- target 表示要被替换的原始函数。
- double 表示替换后的函数(也称为“桩函数”),用它来模拟原始函数的行为。
ApplyFunc 适用于测试时需要完全自定义函数实现的场景。通过传入的 double 函数,开发者可以控制其执行逻辑和返回结果,以便更灵活地测试代码在不同条件下的表现。
patches := gomonkey.ApplyFunc(SomeFunction, func(arg int) int {
return 42 // 自定义实现返回固定值
})
defer patches.Reset()
ApplyFuncReturn
ApplyFuncReturn 将目标函数 target 替换为一个固定的返回值(由 output 提供),而不需自定义函数逻辑。
- target:表示要替换的目标函数。
- output:变参形式,表示目标函数的返回值,可以指定一个或多个返回值(支持多返回值函数)。
ApplyFuncReturn 适用于仅需替换函数返回结果的情况,无需编写额外的实现逻辑,使测试代码更加简洁。
patches := gomonkey.ApplyFuncReturn(SomeFunction, 42, nil) // 返回值为 42 和 nil
defer patches.Reset()
6.2 为公共成员方法打桩
利用以下三个函数可以实现为公共成员方法打桩。
func ApplyMethod(target interface{}, methodName string, double interface{}) *Patches
func ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches
func ApplyMethodReturn(target interface{}, methodName string, output ...interface{}) *Patches
ApplyMethod
ApplyMethod 将目标结构体 target 中的指定方法 methodName 替换为自定义实现 double。
- target:表示包含目标方法的结构体实例。
- methodName:表示目标方法的名称,字符串格式。
- double:替换后的方法实现,通常是一个函数,符合目标方法的签名。
当需要完全替换方法的逻辑时,适合使用 double 函数来实现替代。
type MyStruct struct{}
func (m *MyStruct) MyMethod(arg int) int { return arg * 2 }
// 替换 MyStruct 的 MyMethod
patches := gomonkey.ApplyMethod(&MyStruct{}, "MyMethod", func(_ *MyStruct, arg int) int {
// 模拟行为:原逻辑是 arg*2,现在改为 arg+1
return arg + 1
})
defer patches.Reset()
ApplyMethodFunc
ApplyMethodFunc 类似 ApplyMethod,将 target 中的 methodName 替换为自定义的函数实现。
ApplyMethodFunc 是对 ApplyMethod 的改进,当为 method 打桩时可以不传入 receiver 参数了。
比如对上面 MyStruct 的成员方法 MyMethod 打桩时,需要在桩函数的第一个参数添加接收者。
patches := gomonkey.ApplyMethodFunc(&MyStruct{}, "MyMethod", func(arg int) int {
return arg + 1
})
defer patches.Reset()
ApplyMethodReturn
ApplyMethodReturn 将目标方法 methodName 替换为一个固定的返回值(由 output 提供),不需要自定义逻辑。
- target:表示包含目标方法的结构体实例。
- methodName:目标方法名称。
- output:变长参数,表示目标方法的返回值,可以是多个值(支持多返回值方法)。
ApplyMethodReturn 适合简单的场景,只需要替换方法返回值,而不关心具体的实现逻辑。
patches := gomonkey.ApplyMethodReturn(&MyStruct{}, "MyMethod", 42)
defer patches.Reset()
注意,使用 ApplyFuncReturn 也可以为公有成员方法打桩,语义与其功能不符,所以不建议这么做。
patches := gomonkey.ApplyFuncReturn((*MyStruct).MyMethod, 42)
defer patches.Reset()
6.3 为私有成员方法打桩
ApplyPrivateMethod
利用以下函数可以实现为私有成员方法打桩。
func ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches
ApplyPrivateMethod 函数用于对私有成员方法进行打桩。它的作用是将指定的私有方法替换为自定义的实现,从而在测试中模拟私有方法的行为。
- target:表示包含目标私有方法的结构体实例。
- methodName:私有方法的名称(字符串格式)。私有方法通常以小写字母开头。
- double:替换私有方法的自定义实现。double 必须符合目标方法的签名。
type MyStruct struct{}
func (m *MyStruct) myMethod(arg int) int { return arg * 2 }
// 替换 MyStruct 的 myMethod
patches := gomonkey.ApplyPrivateMethod(&MyStruct{}, "myMethod", func(_ *MyStruct, arg int) int {
return arg + 1
})
defer patches.Reset()
注意:gomonkey 暂未提供 ApplyPrivateMethodReturn 直接返回私有成员方法结果。
6.4 恢复
gomonkey 的打桩函数,一般会返回 Patches 对象。
type Patches struct {
originals map[uintptr][]byte
targets map[uintptr]uintptr
values map[reflect.Value]reflect.Value
valueHolders map[reflect.Value]reflect.Value
}
gomonkey 的 Patches 对象包含对函数或变量的修改,通过调用 Reset,可以撤销这些修改,将代码恢复到未打桩的状态。
通常使用 defer 来确保 Reset 在测试结束时自动执行。
patches := gomonkey.ApplyFunc(SomeFunction, func(arg int) int { return 42 })
defer patches.Reset()
7.小结
gomonkey 是一款开源的 Go 语言打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。
gomonkey 接口友好,功能强大,目前已被很多项目使用,用户遍及世界多个国家。
关于 gomonkey 更多的用法,比如对接口的打桩,函数变量的打桩,以及全局变量的打桩,请参阅官方文档。
参考文献
Monkey patch - Wikipedia
Monkey Patch(猴子补丁) - Learn Python
你该刷新gomonkey 的惯用法了 - 煎鱼