本文主要针对单元测试工具,其他工具请看专栏内其它博客。
gomonkey
介绍:gomonkey是一款强大的运行时打桩(Mock)工具/动态 Mock 工具,能够在不修改源代码的前提下,对函数、方法、全局变量等进行动态替换,广泛用于单元测试场景。
工具很全面,可以针对数据库,外部请求http接口,变量和结构体等打桩,不过个人认为对于http接口和数据库还是使用上面的两个方法要方便一些
优点:
- 无侵入式打桩,无需修改业务代码
- 功能全面,支持函数、方法、全局变量等多种打桩场景
- 支持私有成员打桩,适配遗留项目
- 轻量级易用,API 简洁,兼容主流框架
- 灵活控制打桩生命周期,精准适配测试需求
- x86_64 (Intel/AMD) 架构下,功能基本完整、稳定,是生产级 Mock 工具
致命缺陷:
- 对Windows、Mac、arm架构系统支持很不友好
- 在 ARM64 (aarch64) 架构下,存在 大量核心功能失效、运行时崩溃、Mock 无效果 的缺陷
- 缺陷是 gomonkey 的底层实现原理 导致的,而非 Go 语言 / ARM64 的兼容性问题,官方至今未彻底修复
- Go1.18 + 版本 中更严重,Go1.17 及以下 ARM64 版本问题稍少,但依然存在关键缺陷
1.安装
go get github.com/agiledragon/gomonkey/v2
2.方法简介
官方推荐命令(禁用内联优化):go test -gcflags=all=-l
gomonkey有两种调用形式:
- 全局函数调用
- 结构体方法调用
区别:
- 归属关系完全不同:前者包级别全局函数,不属于任何结构体,直接通过包名调用即可;后者结构体的指针接收者成员方法,属于结构体的一部分,必须通过
Patches结构体的实例对象(指针)才能调用。 - 调用前置条件不同:前者调用前,不需要手动创建
Patches实例,全局函数内部帮你自动创建,直接调用即可;后者调用前,必须先手动创建一个Patches实例 - 底层执行逻辑 - 最核心的调用链路不同:
- 全局函数的执行链路(一行顶三步)
- 步骤1:内部调用 create() 创建一个全新的空 Patches 实例
- 步骤2:立刻调用这个新实例的 成员方法 ApplyMethodFunc
- 步骤3:返回这个实例对象本身(链式调用基础)
- 补充:源码中的
create()等价于NewPatches(),都是初始化空的 Patches 结构体
- 结构体成员方法的执行链路
- 根据入参
target(结构体 / 结构体指针 /reflect.Type)和methodName,反射获取目标方法; - 通过
funcToMethod做「普通函数 → 结构体方法」的转换(核心逻辑,后面讲); - 调用
ApplyCore执行底层的内存指令改写(打桩核心:函数地址跳转); - 返回
Patches实例本身,支持链式调用。 - 补充:是真实的核心实现,做了所有的实际工作
- 根据入参
- 全局函数的执行链路(一行顶三步)
共同点:
- 最终实现的业务功能完全一致:都是为「结构体的指定方法」打桩,替换原方法的执行逻辑;
- 入参校验、底层打桩逻辑完全一致:全局函数只是转发调用,所有的校验(方法是否存在、函数签名是否匹配)、内存指令改写,都是结构体成员方法做的;
- 都支持链式调用:返回值都是
*Patches,都可以继续追加.ApplyXXX()系列方法; - 都需要手动调用
Reset()还原桩:不管是全局函数返回的实例,还是手动创建的实例,最终都要调用Reset(),否则会导致后续测试被污染。
方法目录:
官方包源码文件,地址
gopackage gomonkey import ( "fmt" "reflect" "syscall" "unsafe" "github.com/agiledragon/gomonkey/v2/creflect" ) type Patches struct { originals map[uintptr][]byte targets map[uintptr]uintptr values map[reflect.Value]reflect.Value valueHolders map[reflect.Value]reflect.Value } type Params []interface{} type OutputCell struct { Values Params Times int } func ApplyFunc(target, double interface{}) *Patches { return create().ApplyFunc(target, double) } func ApplyMethod(target interface{}, methodName string, double interface{}) *Patches { return create().ApplyMethod(target, methodName, double) } func ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches { return create().ApplyMethodFunc(target, methodName, doubleFunc) } func ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches { return create().ApplyPrivateMethod(target, methodName, double) } func ApplyGlobalVar(target, double interface{}) *Patches { return create().ApplyGlobalVar(target, double) } func ApplyFuncVar(target, double interface{}) *Patches { return create().ApplyFuncVar(target, double) } func ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches { return create().ApplyFuncSeq(target, outputs) } func ApplyMethodSeq(target interface{}, methodName string, outputs []OutputCell) *Patches { return create().ApplyMethodSeq(target, methodName, outputs) } func ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches { return create().ApplyFuncVarSeq(target, outputs) } func ApplyFuncReturn(target interface{}, output ...interface{}) *Patches { return create().ApplyFuncReturn(target, output...) } func ApplyMethodReturn(target interface{}, methodName string, output ...interface{}) *Patches { return create().ApplyMethodReturn(target, methodName, output...) } func ApplyFuncVarReturn(target interface{}, output ...interface{}) *Patches { return create().ApplyFuncVarReturn(target, output...) } func create() *Patches { return &Patches{originals: make(map[uintptr][]byte), targets: map[uintptr]uintptr{}, values: make(map[reflect.Value]reflect.Value), valueHolders: make(map[reflect.Value]reflect.Value)} } func NewPatches() *Patches { return create() } func (this *Patches) Origin(fn func()) { for target, bytes := range this.originals { modifyBinary(target, bytes) } fn() for target, targetPtr := range this.targets { code := buildJmpDirective(targetPtr) modifyBinary(target, code) } } func (this *Patches) ApplyFunc(target, double interface{}) *Patches { t := reflect.ValueOf(target) d := reflect.ValueOf(double) return this.ApplyCore(t, d) } func (this *Patches) ApplyMethod(target interface{}, methodName string, double interface{}) *Patches { m, ok := castRType(target).MethodByName(methodName) if !ok { panic("retrieve method by name failed") } d := reflect.ValueOf(double) return this.ApplyCore(m.Func, d) } func (this *Patches) ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches { m, ok := castRType(target).MethodByName(methodName) if !ok { panic("retrieve method by name failed") } d := funcToMethod(m.Type, doubleFunc) return this.ApplyCore(m.Func, d) } func (this *Patches) ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches { m, ok := creflect.MethodByName(castRType(target), methodName) if !ok { panic("retrieve method by name failed") } d := reflect.ValueOf(double) return this.ApplyCoreOnlyForPrivateMethod(m, d) } func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches { t := reflect.ValueOf(target) if t.Type().Kind() != reflect.Ptr { panic("target is not a pointer") } this.values[t] = reflect.ValueOf(t.Elem().Interface()) d := reflect.ValueOf(double) t.Elem().Set(d) return this } func (this *Patches) ApplyFuncVar(target, double interface{}) *Patches { t := reflect.ValueOf(target) d := reflect.ValueOf(double) if t.Type().Kind() != reflect.Ptr { panic("target is not a pointer") } this.check(t.Elem(), d) return this.ApplyGlobalVar(target, double) } func (this *Patches) ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches { funcType := reflect.TypeOf(target) t := reflect.ValueOf(target) d := getDoubleFunc(funcType, outputs) return this.ApplyCore(t, d) } func (this *Patches) ApplyMethodSeq(target interface{}, methodName string, outputs []OutputCell) *Patches { m, ok := castRType(target).MethodByName(methodName) if !ok { panic("retrieve method by name failed") } d := getDoubleFunc(m.Type, outputs) return this.ApplyCore(m.Func, d) } func (this *Patches) ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches { t := reflect.ValueOf(target) if t.Type().Kind() != reflect.Ptr { panic("target is not a pointer") } if t.Elem().Kind() != reflect.Func { panic("target is not a func") } funcType := reflect.TypeOf(target).Elem() double := getDoubleFunc(funcType, outputs).Interface() return this.ApplyGlobalVar(target, double) } func (this *Patches) ApplyFuncReturn(target interface{}, returns ...interface{}) *Patches { funcType := reflect.TypeOf(target) t := reflect.ValueOf(target) outputs := []OutputCell{{Values: returns, Times: -1}} d := getDoubleFunc(funcType, outputs) return this.ApplyCore(t, d) } func (this *Patches) ApplyMethodReturn(target interface{}, methodName string, returns ...interface{}) *Patches { m, ok := reflect.TypeOf(target).MethodByName(methodName) if !ok { panic("retrieve method by name failed") } outputs := []OutputCell{{Values: returns, Times: -1}} d := getDoubleFunc(m.Type, outputs) return this.ApplyCore(m.Func, d) } func (this *Patches) ApplyFuncVarReturn(target interface{}, returns ...interface{}) *Patches { t := reflect.ValueOf(target) if t.Type().Kind() != reflect.Ptr { panic("target is not a pointer") } if t.Elem().Kind() != reflect.Func { panic("target is not a func") } funcType := reflect.TypeOf(target).Elem() outputs := []OutputCell{{Values: returns, Times: -1}} double := getDoubleFunc(funcType, outputs).Interface() return this.ApplyGlobalVar(target, double) } func (this *Patches) Reset() { for target, bytes := range this.originals { modifyBinary(target, bytes) delete(this.originals, target) } for target, variable := range this.values { target.Elem().Set(variable) } } func (this *Patches) ApplyCore(target, double reflect.Value) *Patches { this.check(target, double) assTarget := *(*uintptr)(getPointer(target)) original := replace(assTarget, uintptr(getPointer(double))) if _, ok := this.originals[assTarget]; !ok { this.originals[assTarget] = original } this.targets[assTarget] = uintptr(getPointer(double)) this.valueHolders[double] = double return this } func (this *Patches) ApplyCoreOnlyForPrivateMethod(target unsafe.Pointer, double reflect.Value) *Patches { if double.Kind() != reflect.Func { panic("double is not a func") } assTarget := *(*uintptr)(target) original := replace(assTarget, uintptr(getPointer(double))) if _, ok := this.originals[assTarget]; !ok { this.originals[assTarget] = original } this.targets[assTarget] = uintptr(getPointer(double)) this.valueHolders[double] = double return this } func (this *Patches) check(target, double reflect.Value) { if target.Kind() != reflect.Func { panic("target is not a func") } if double.Kind() != reflect.Func { panic("double is not a func") } targetType := target.Type() doubleType := double.Type() if targetType.NumIn() < doubleType.NumIn() || targetType.NumOut() != doubleType.NumOut() || (targetType.NumIn() == doubleType.NumIn() && targetType.IsVariadic() != doubleType.IsVariadic()) { panic(fmt.Sprintf("target type(%s) and double type(%s) are different", target.Type(), double.Type())) } for i, size := 0, doubleType.NumIn(); i < size; i++ { targetIn := targetType.In(i) doubleIn := doubleType.In(i) if targetIn.AssignableTo(doubleIn) { continue } panic(fmt.Sprintf("target type(%s) and double type(%s) are different", target.Type(), double.Type())) } for i, size := 0, doubleType.NumOut(); i < size; i++ { targetOut := targetType.Out(i) doubleOut := doubleType.Out(i) if targetOut.AssignableTo(doubleOut) { continue } panic(fmt.Sprintf("target type(%s) and double type(%s) are different", target.Type(), double.Type())) } } func replace(target, double uintptr) []byte { code := buildJmpDirective(double) bytes := entryAddress(target, len(code)) original := make([]byte, len(bytes)) copy(original, bytes) modifyBinary(target, code) return original } func getDoubleFunc(funcType reflect.Type, outputs []OutputCell) reflect.Value { if funcType.NumOut() != len(outputs[0].Values) { panic(fmt.Sprintf("func type has %v return values, but only %v values provided as double", funcType.NumOut(), len(outputs[0].Values))) } needReturn := false slice := make([]Params, 0) for _, output := range outputs { if output.Times == -1 { needReturn = true slice = []Params{output.Values} break } t := 0 if output.Times <= 1 { t = 1 } else { t = output.Times } for j := 0; j < t; j++ { slice = append(slice, output.Values) } } i := 0 lenOutputs := len(slice) return reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value { if needReturn { return GetResultValues(funcType, slice[0]...) } if i < lenOutputs { i++ return GetResultValues(funcType, slice[i-1]...) } panic("double seq is less than call seq") }) } func GetResultValues(funcType reflect.Type, results ...interface{}) []reflect.Value { var resultValues []reflect.Value for i, r := range results { var resultValue reflect.Value if r == nil { resultValue = reflect.Zero(funcType.Out(i)) } else { v := reflect.New(funcType.Out(i)) v.Elem().Set(reflect.ValueOf(r)) resultValue = v.Elem() } resultValues = append(resultValues, resultValue) } return resultValues } type funcValue struct { _ uintptr p unsafe.Pointer } func getPointer(v reflect.Value) unsafe.Pointer { return (*funcValue)(unsafe.Pointer(&v)).p } func entryAddress(p uintptr, l int) []byte { return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{Data: p, Len: l, Cap: l})) } func pageStart(ptr uintptr) uintptr { return ptr & ^(uintptr(syscall.Getpagesize() - 1)) } func funcToMethod(funcType reflect.Type, doubleFunc interface{}) reflect.Value { rf := reflect.TypeOf(doubleFunc) if rf.Kind() != reflect.Func { panic("doubleFunc is not a func") } vf := reflect.ValueOf(doubleFunc) return reflect.MakeFunc(funcType, func(in []reflect.Value) []reflect.Value { if funcType.IsVariadic() { return vf.CallSlice(in[1:]) } else { return vf.Call(in[1:]) } }) } func castRType(val interface{}) reflect.Type { if rTypeVal, ok := val.(reflect.Type); ok { return rTypeVal } return reflect.TypeOf(val) }
3.使用示例
示例**(1)**使用方法一全局函数调用,其余使用结构体方法调用。
(1)函数打桩方法
gomonkey.ApplyFunc()
相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头
参数示例:
go
// 第一个参数:函数名
// 第二个参数:打桩函数,入参和出参要保持和被打桩函数保持一致
func ApplyFunc(target, double interface{}) *Patches {
return create().ApplyFunc(target, double)
}
user.go
go
package func_demo
import "tracer/model/sqlmock_demo"
// GetUserInfo 查询所有用户信息
func GetUserInfo() (interface{}, error) {
obj, err := sqlmock_demo.GetAllUser()
if err != nil {
return nil, err
}
return obj, nil
}
user_test.go
go
package func_demo
import (
"errors"
"fmt"
"github.com/agiledragon/gomonkey/v2"
"gorm.io/gorm"
"testing"
"tracer/model"
"tracer/model/sqlmock_demo"
)
// TestUserInfo 单个函数通过循环覆盖所有测试场景
func TestUserInfo(t *testing.T) {
// 1. 定义测试用例结构体:封装输入(打桩参数)和预期输出
type testCase struct {
name string // 用例名称,便于排查错误
mockUsers []sqlmock_demo.UserInfo // 打桩 GetAllUser 返回的用户列表
mockErr error // 打桩 GetAllUser 返回的错误
expectedErr error // 预期 UserInfoDao 返回的错误
expectedNil bool // 预期 UserInfoDao 返回的数据是否为 nil
expectedCount int // 预期返回的用户数量(正常场景有效)
}
// 2. 构造所有测试用例(正常场景 + 异常场景)
testCases := []testCase{
{
name: "正常场景-返回2个用户",
mockUsers: []sqlmock_demo.UserInfo{
{
Model: gorm.Model{ID: 1},
UserName: "zhangsan",
Password: "123456",
Phone: "13800138000",
Email: "zhangsan@test.com",
},
{
Model: gorm.Model{ID: 2},
UserName: "lisi",
Password: "654321",
Phone: "13900139000",
Email: "lisi@test.com",
},
},
mockErr: nil,
expectedErr: nil,
expectedNil: false,
expectedCount: 2,
},
{
name: "正常场景-返回空用户列表",
mockUsers: []sqlmock_demo.UserInfo{},
mockErr: nil,
expectedErr: nil,
expectedNil: false,
expectedCount: 0,
},
{
name: "异常场景-GORM记录不存在错误",
mockUsers: nil,
mockErr: gorm.ErrRecordNotFound,
expectedErr: gorm.ErrRecordNotFound,
expectedNil: true,
expectedCount: 0,
},
{
name: "异常场景-自定义查询错误",
mockUsers: nil,
mockErr: errors.New("数据库连接超时"),
expectedErr: errors.New("数据库连接超时"),
expectedNil: true,
expectedCount: 0,
},
}
// 3. 循环执行所有测试用例
for _, tc := range testCases {
model.InitDb()
// t.Run:为每个用例创建独立的测试上下文,互不干扰,便于定位用例错误
t.Run(tc.name, func(t *testing.T) {
// 步骤1:对 GetAllUser 进行动态打桩(每个用例独立打桩,避免相互影响)
// 使用ApplyFunc打桩跨包函数
patches := gomonkey.ApplyFunc(sqlmock_demo.GetAllUser, func() ([]sqlmock_demo.UserInfo, error) {
// 返回当前用例预设的模拟数据和错误
return tc.mockUsers, tc.mockErr
})
defer patches.Reset() // 每个用例执行完毕后重置打桩,避免污染其他用例
// 步骤2:执行待测试函数 GetUserInfo
_, err := GetUserInfo()
if err != nil {
fmt.Println(err)
}
})
}
}
命令行执行命令
go test -cover -gcflags=all=-l -covermode=atomic
结果:
powershell
PS D:\wyl\workspace\go\tracer\logic\func_demo> go test -cover
PASS
coverage: 75.0% of statements
ok tracer/logic/func_demo 0.088s
如果报错,这个问题是数据库中不存在表:
Error 1146 (42S02): Table 'tracer.user_info' doesn't exist
(2)结构体方法打桩方法
gomonkey.ApplyMethod()、gomonkey.ApplyMethodFunc()
区别:
- 匹配方式不同 :
ApplyMethod是名称匹配,ApplyMethodFunc是函数本体匹配 - 传参核心不同 :
ApplyMethod必须传reflect.Type+方法名字符串;ApplyMethodFunc直接传原方法函数,不需要反射 - 底层逻辑不同 :
ApplyMethod是「反射查找方法」,ApplyMethodFunc是「直接绑定方法函数」,后者性能更高
gomonkey.ApplyMethod() 参数示例:
go
// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致,但需注意需要额外传入结构体类型且必须是第一个参数
func ApplyMethod(target interface{}, methodName string, double interface{}) *Patches {
return create().ApplyMethod(target, methodName, double)
}
gomonkey.ApplyMethodFunc() 参数示例:
go
// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致,不需要额外参数
func ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches {
return create().ApplyMethodFunc(target, methodName, doubleFunc)
}
**注意:**可以不用写reflect.TypeOf(实例),如果了解方法所属类型,可以直接写方法类型,而不用reflect.TypeOf()在获取一次
gomonkey.ApplyMethod()
method_demo.go
go
type MethodDemo struct {
}
func (m MethodDemo) MethodDemo(ret string) {
fmt.Println("MethodDemo:", ret)
}
method_demo_test.go
go
// ========== 基于 gomonkey.ApplyMethod() 的单元测试 ==========
func TestMethodDemo(t *testing.T) {
// 1. 初始化结构体实例(当前结构体无成员变量,直接实例化即可)
md := MethodDemo{}
patches := gomonkey.NewPatches()
// 铁律:延迟撤销打桩,防止污染其他测试用例,必写!
defer patches.Reset()
// 2. 核心:使用 gomonkey.ApplyMethod() 对【值接收者方法】打桩
// 第一个参数:reflect.TypeOf(实例) → 因为是值接收者,直接传值类型实例即可
// 第二个参数:被打桩的方法名字符串(严格和原方法名一致,大小写敏感)
// 第三个参数:mock桩函数 → 入参/返回值 必须和原方法完全一致
patches.ApplyMethod(
// 可以直接写 MethodDemo{}
reflect.TypeOf(md),
"MethodDemo",
func(m MethodDemo, ret string) {
// 自定义的mock逻辑,替代原方法的 fmt.Println 逻辑
t.Log("mock执行成功,入参ret:", ret)
},
)
// 3. 调用原方法,验证打桩是否生效
md.MethodDemo("hello gomonkey")
}
命令行执行命令
go test -run "^TestMethodDemo$" -cover
结果:
powershell
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodDemo$" -cover
MethodDemo: hello gomonkey
PASS
coverage: 50.0% of statements
ok tracer/logic/method_demo 0.303s
gomonkey.ApplyMethodFunc()
method_func_demo.go
go
type MethodFuncDemo struct {
}
func (m MethodFuncDemo) MethodFuncDemo(ret string) {
fmt.Println("MethodFuncDemo:", ret)
}
method_func_demo_test.go
go
// ========== 【结构体值类型绑定】对应的单元测试(纯值类型,无任何指针语法) ==========
func TestMethodFuncDemo(t *testing.T) {
// 1. 初始化【结构体值类型实例】 核心✅ 无指针&,纯结构体类型绑定
mfd := MethodFuncDemo{}
patches := gomonkey.NewPatches()
// 铁律:延迟撤销打桩,防止污染其他测试用例,必写!
defer patches.Reset()
// 2. 核心:gomonkey.ApplyMethodFunc 三参数打桩【结构体值类型绑定】
// 三参数固定规则:值类型 = 值实例 + 方法名字符串 + 值类型桩函数
patches.ApplyMethodFunc(
mfd, // 参数1:结构体值类型实例(核心,纯值绑定)
"MethodFuncDemo", // 参数2:方法名字符串(和值接收者方法名一致)
func(ret string) { // 参数3:桩函数【无*号,纯结构体值类型入参】✅必匹配
// 桩函数第一个入参必须是:纯结构体类型 MethodFuncDemo,无任何指针
t.Log("✅ 结构体值类型绑定打桩生效,入参ret = ", ret)
},
)
// 3. 调用【值接收者方法】,验证结构体值类型绑定打桩结果
mfd.MethodFuncDemo("hello gomonkey 结构体值类型绑定")
}
命令行执行命令
go test -run "^TestMethodFuncDemo$" -gcflags=all=-l
结果:
powershell
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncDemo$"
MethodFuncDemo: hello gomonkey 结构体值类型绑定
PASS
ok tracer/logic/method_demo 0.243s
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncDemo$" -gcflags=all=-l
PASS
ok tracer/logic/method_demo 0.219s
(3)指针接收者方法打桩方法
gomonkey.ApplyMethod()、gomonkey.ApplyMethodFunc()
区别:
- 匹配方式不同 :
ApplyMethod是名称匹配,ApplyMethodFunc是函数本体匹配 - 传参核心不同 :
ApplyMethod必须传reflect.Type+方法名字符串;ApplyMethodFunc直接传原方法函数,不需要反射 - 底层逻辑不同 :
ApplyMethod是「反射查找方法」,ApplyMethodFunc是「直接绑定方法函数」,后者性能更高
gomonkey.ApplyMethod() 参数示例:
go
// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致,但需注意需要额外传入结构体类型且必须是第一个参数
func ApplyMethod(target interface{}, methodName string, double interface{}) *Patches {
return create().ApplyMethod(target, methodName, double)
}
gomonkey.ApplyMethodFunc() 参数示例:
go
// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致,不需要额外参数
func ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches {
return create().ApplyMethodFunc(target, methodName, doubleFunc)
}
**注意:**可以不用写reflect.TypeOf(实例),如果了解方法所属类型,可以直接写方法类型,而不用reflect.TypeOf()在获取一次
gomonkey.ApplyMethod()
method_demo.go
go
type MethodDemo struct {
}
func (m MethodDemo) MethodDemo(ret string) {
fmt.Println("MethodDemo:", ret)
}
func (m *MethodDemo) MethodPointerDemo(ret string) {
fmt.Println("MethodPointerDemo:", ret)
}
method_demo_test.go
go
// ========== 基于 gomonkey.ApplyMethod() 的单元测试【指针接收者专用写法】 ==========
func TestMethodPointerDemo(t *testing.T) {
// 1. 初始化【指针类型】的结构体实例 (必须是指针,和方法接收者对应)
md := &MethodDemo{}
patches := gomonkey.NewPatches()
// 铁律:延迟撤销打桩,防止污染其他测试用例,必写!
defer patches.Reset()
// 2. 核心:gomonkey.ApplyMethod() 打桩【指针接收者方法】
// 第一个参数:reflect.TypeOf(md) 传入指针实例,获取 *MethodDemo 的反射类型 【必须传指针实例】
// 第二个参数:方法名字符串,严格大小写一致
// 第三个参数:mock桩函数,入参规则严格匹配
patches.ApplyMethod(
// 可以直接写 &MethodDemo{}
reflect.TypeOf(md),
"MethodPointerDemo",
// mock桩函数规则:
// ① 第一个入参:必须是【指针接收者】 *MethodDemo ,和原方法一致
// ② 第二个入参:原方法的入参 ret string,和原方法一致
// ③ 原方法无返回值,桩函数也必须无返回值
func(m *MethodDemo, ret string) {
// 自定义mock逻辑,替代原方法的 fmt.Println 逻辑
t.Log("✅ 指针方法打桩生效,入参ret = ", ret)
},
)
// 3. 调用被打桩的指针方法,验证mock是否生效
md.MethodPointerDemo("hello gomonkey 指针接收者")
}
命令行执行命令
go test -run "^TestMethodPointerDemo$" -gcflags=all=-l
结果:
powershell
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodPointerDemo$" -cover
MethodPointerDemo: hello gomonkey 指针接收者
PASS
coverage: 25.0% of statements
ok tracer/logic/method_demo 0.277s
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodPointerDemo$" -gcflags=all=-l
PASS
ok tracer/logic/method_demo 0.266s
gomonkey.ApplyMethodFunc()
method_func_demo.go
go
type MethodFuncDemo struct {
}
func (m MethodFuncDemo) MethodFuncDemo(ret string) {
fmt.Println("MethodFuncDemo:", ret)
}
func (m *MethodFuncDemo) MethodFuncPrinterDemo(ret string) {
fmt.Println("MethodFuncPrinterDemo:", ret)
}
method_func_demo_test.go
go
// ========== 指针接收者 对应的 ApplyMethodFunc 单元测试 ==========
func TestMethodFuncPointerDemo(t *testing.T) {
// 1. 初始化指针类型的结构体实例
mfd := &MethodFuncDemo{}
patches := gomonkey.NewPatches()
// 铁律:延迟撤销打桩,防止污染其他测试用例,必写!
defer patches.Reset()
// 2. 核心:gomonkey.ApplyMethodFunc 打桩【指针接收者方法】
// ✅ 第一个参数:直接传【指针接收者的方法本体】 语法固定:(*结构体名).方法名
patches.ApplyMethodFunc(
// 重点:指针接收者的方法本体写法
mfd,
"MethodFuncPrinterDemo",
func(m *MethodFuncDemo, ret string) {
// 桩函数第一个入参必须是 指针类型 *MethodFuncDemo
t.Log("✅ MethodFuncPrinterDemo 指针接收者打桩生效,入参ret = ", ret)
},
)
// 3. 调用指针方法
mfd.MethodFuncPrinterDemo("hello gomonkey 指针接收者")
}
命令行执行命令
go test -run "^TestMethodFuncPointerDemo$" -gcflags=all=-l
结果:
powershell
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncPointerDemo$" -cover
MethodFuncPrinterDemo: hello gomonkey 指针接收者
PASS
coverage: 25.0% of statements
ok tracer/logic/method_demo 0.275s
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncPointerDemo$" -gcflags=all=-l
PASS
ok tracer/logic/method_demo 0.227s
(4)多次调用,顺序返回不同结果
gomonkey.ApplyMethodSeq()(高频实用)方法
处理「结构体方法被多次调用,需要返回不同结果」的核心方法,完美解决:方法第 1 次调用返回 A、第 2 次返回 B、第 N 次返回默认值 / 报错 这类高频业务场景。
这个也和上面(2)和(3)一样支持两种写法,函数和结构体方法,写法类似,这里就只写一个示例了
参数示例:
go
// 存储单次调用的返回值集合
type Params []interface{}
type OutputCell struct {
Values Params // 本次要返回的参数,个数/类型必须和原方法返回值完全一致
Times int // 生效次数:-1=永久生效(匹配后后续调用全用这个);>0=生效指定次数
}
// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法返回值切片,入参和出参要保持和被打桩方法保持一致
func (this *Patches) ApplyMethodSeq(target interface{}, methodName string, outputs []OutputCell) *Patches {
m, ok := castRType(target).MethodByName(methodName)
if !ok {
panic("retrieve method by name failed")
}
d := getDoubleFunc(m.Type, outputs)
return this.ApplyCore(m.Func, d)
}
product.go
go
package method_demo
import "fmt"
// ========== 1. 定义业务结构体和待打桩的方法 ==========
type Product struct {
Id int
Name string
Stock int
}
// 结构体公有方法(首字母大写):库存查询,返回【库存数量、错误信息】
func (p *Product) GetStock() (int, error) {
// 真实业务逻辑:查询数据库/缓存获取库存
return p.Stock, nil
}
// ========== 2. 业务逻辑:连续调用3次GetStock方法 ==========
// 模拟业务中多次调用结构体方法的场景
func QueryStockMultiTimes(p *Product) {
// 第1次调用
stock1, err1 := p.GetStock()
fmt.Printf("第1次查询库存: %d, err: %v\n", stock1, err1)
// 第2次调用
stock2, err2 := p.GetStock()
fmt.Printf("第2次查询库存: %d, err: %v\n", stock2, err2)
// 第3次调用
stock3, err3 := p.GetStock()
fmt.Printf("第3次查询库存: %d, err: %v\n", stock3, err3)
}
product_test.go
go
package method_demo
import (
"errors"
"github.com/agiledragon/gomonkey/v2"
"testing"
)
func TestProduct(t *testing.T) {
// 1. 创建patches实例,必须写defer Reset(),保证打桩还原,无残留
patches := gomonkey.NewPatches()
defer patches.Reset()
// 2. 初始化结构体实例
prod := &Product{Id: 1, Name: "苹果手机", Stock: 200}
// 3. 核心:定义序列打桩规则
stockSeq := []gomonkey.OutputCell{
{Values: gomonkey.Params{100, nil}, Times: 1}, // 规则1:第1次调用,返回100,nil
{Values: gomonkey.Params{0, nil}, Times: 1}, // 规则2:第2次调用,返回0,nil
{Values: gomonkey.Params{0, errors.New("库存不足,无法下单")}, Times: 1}, // 规则3:永久生效(-1)这个参数会导致严重bug,避免使用
}
// 4. 执行方法序列打桩
patches.ApplyMethodSeq(prod, "GetStock", stockSeq)
// 5. 执行业务逻辑,触发多次调用
QueryStockMultiTimes(prod)
}
命令行执行命令
go test -run "^TestProduct$" -cover -gcflags=all=-l -covermode=atomic
结果:
powershell
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestProduct$" -cover -gcflags=all=-l -covermode=atomic
第1次查询库存: 100, err: <nil>
第2次查询库存: 0, err: <nil>
第3次查询库存: 0, err: 库存不足,无法下单
PASS
coverage: 54.5% of statements
ok tracer/logic/method_demo 0.330s
(5)结构体私有方法打桩方法
gomonkey.ApplyPrivateMethod()
私有方法打桩方案:
- 方案一:【最优推荐,零侵入、Go 官方标准】测试文件和业务文件放在同一个包下(首选,无任何副作用)
- 方案二:【妥协方案,少量侵入】把「需要 mock 的私有方法」改为包级私有函数(适合特殊场景)
- 方案三:【不推荐,侵入性大】把私有方法改为公有方法(万不得已才用)
ApplyPrivateMethod 和 ApplyMethod 打私有方法区别:功能上完全一致,同包下都能正常打桩、正常 mock 逻辑。唯一的区别是:
ApplyPrivateMethod:语义精准,一看就知道是打「私有方法」,符合官方设计;ApplyMethod:语义模糊,它的设计初衷是打「公有方法」,打私有方法只是兼容生效。
这个也和上面(2)和(3)一样支持两种写法,函数和结构体方法,写法类似,这里就只写一个示例了
参数示例:
go
// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致
func (this *Patches) ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches {
m, ok := creflect.MethodByName(castRType(target), methodName)
if !ok {
panic("retrieve method by name failed")
}
d := reflect.ValueOf(double)
return this.ApplyCoreOnlyForPrivateMethod(m, d)
}
student.go
go
package private_demo
type Student struct{}
// 公有方法(对外暴露)
func (s Student) Study(course string) bool {
// 内部调用了私有方法
if s.checkCourse(course) { // 私有方法,首字母小写
s.recordLog(course) // 私有方法
return true
}
return false
}
// 私有方法(内部逻辑)
func (s Student) checkCourse(course string) bool {
return course != ""
}
func (s Student) recordLog(course string) {}
student_test.go
go
package private_demo
import (
"github.com/agiledragon/gomonkey/v2"
"testing"
)
// TODO: TestStudy有问题暂时不确定是因为不支持这种写法还是代码有问题
func TestStudy(t *testing.T) {
s := &Student{}
patches := gomonkey.NewPatches()
defer patches.Reset()
// ✅ 可以直接用gomonkey打桩【私有方法】checkCourse,零权限问题!
patches.ApplyMethod(&Student{}, "checkCourse", func(_ *Student, course string) bool {
return true // mock私有方法返回true
})
res := s.Study("math")
if !res {
t.Error("测试失败")
}
}
// 测试代码:指针接收者的私有方法打桩
func TestStudyPrivate(t *testing.T) {
s := &Student{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyPrivateMethod(
(*Student)(nil), // ✅ 指针接收者传 (*结构体)(nil)
"checkCourse", // 私有方法名不变
func(_ *Student, course string) bool { // ✅ 桩函数接收者是指针类型
return true
},
)
res := s.Study("math")
if !res {
t.Error("测试失败")
}
}
命令行执行命令
go test -run "^TestStudyPrivate$" -gcflags=all=-l
结果:
powershell
PS D:\wyl\workspace\go\tracer\logic\private_demo> go test -run "^TestStudyPrivate$" -gcflags=all=-l
PASS
ok tracer/logic/private_demo 0.210s