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.安装
github地址
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(),否则会导致后续测试被污染。
方法目录:
官方包源码文件,地址
package gomonkeyimport ("fmt""reflect""syscall""unsafe""github.com/agiledragon/gomonkey/v2/creflect")type Patches struct {originals map[uintptr][]bytetargets map[uintptr]uintptrvalues map[reflect.Value]reflect.ValuevalueHolders map[reflect.Value]reflect.Value}type Params []interface{}type OutputCell struct {Values ParamsTimes 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] = doublereturn 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] = doublereturn 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 := falseslice := make([]Params, 0)for _, output := range outputs {if output.Times == -1 {needReturn = trueslice = []Params{output.Values}break}t := 0if output.Times <= 1 {t = 1} else {t = output.Times}for j := 0; j < t; j++ {slice = append(slice, output.Values)}}i := 0lenOutputs := 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.Valuefor i, r := range results {var resultValue reflect.Valueif 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 {_ uintptrp 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)}
