1.问题起源
今天想对之前匆匆写下的代码完善单元测试,使用了gomonkey对代码中的http调用和rpc调用进行打桩,核心的待测代码和测试代码如下:
package utils
type MaterialUtil interface {
QueryTaskByTaskId(ctx context.Context, centerTaskId int64) (resp *MaterialTask, err error)
}
type MaterialUtilImpl struct {
}
func (m MaterialUtilImpl) QueryTaskByTaskId(ctx context.Context, centerTaskId int64) (resp *MaterialTask, err error) {
resp, err = getMaterialsByTaskId(centerTaskId)
return resp, err
}
package material_test
import (
"context"
"github.com/agiledragon/gomonkey"
. "github.com/smartystreets/goconvey/convey"
"reflect"
"testing"
"utils"
)
func TestMaterialCenterTask(t *testing.T) {
Convey("test material task center", t, func() {
gomonkey.ApplyMethod(reflect.TypeOf(&utils.MaterialUtilImpl{}), "QueryTaskByTaskId", func(_ *utils.MaterialUtilImpl, ctx context.Context, centerTaskId int64) (resp *utils.MaterialTask, err error) {
return &utils.MaterialTask{
ErrCode: 0,
Msg: "mock调用成功",
}, nil
})
materialUtil := &utils.MaterialUtilImpl{}
resp, err := materialUtil.QueryTaskByTaskId(context.TODO(), 12345)
So(err, ShouldBeNil)
So(resp.Msg, ShouldEqual, "mock调用成功")
})
}
单测不通过,报错,说明gomonkey ApplyMethod不生效,没有正常mock MaterialUtilImpl的QueryTaskByTaskId方法。
Failures:
Expected: 'mock调用成功'
Actual: 'null'
(Should be equal)
2.解决方式
1.Goland配置test禁止内联优化
gomonkey对于mock方法失败的官方解决方式是启动go test时新增启动参数-gcflags=all=-l 避免内联优化。 gomonkey的实现原理是获取目标函数的入口地址并将跳转至替换函数的机器码填充在目标函数的地址处,这样调用目标函数时,从目标函数的入口地址进入会直接执行go monkey填充的跳转指令,跳转到替换函数处继续执行。因此,需要在调用目标函数时,跳转至目标函数的入口地址,才可以执行go monkey填充的跳转指令。而golang编译器在编译时会进行内联优化,即把简短的函数在调用它的地方展开,从而消除调用目标函数的开销。但因为内联消除了调用目标函数时的跳转操作,使得go monkey填充在目标函数入口处的指令无法执行,因而也无法实现函数体的运行时替换,使go monkey失效。
使用goland的话在Run/Edit Configuraitons中也可以新增这个启动参数,在执行测试时附加上:
2.修改指针/结构体调用
做完上一步后发现还是不行,再次看文档示例发现一个不一样的地方,就是示例中默认认为大家都是通过结构体指针调用方法,而不是通过结构体调用方法的,但是在我们的代码实现中是会出现通过结构体调用方法的,这就是问题所在:
使用ApplyMethod时,reflect.TypeOf(caller)的caller入参和func(_ caller)的caller入参必须和原方法一致,原方法采用的是结构体调用,那么caller就必须为结构体,反之就都得为指针。
所以解决方式有两种,一种是修改原接口为指针调用,那么上述test程序就可以正确mock了,第二种是在ApplyMethod时采用结构体作为入参:
package material_test
import (
"context"
"github.com/agiledragon/gomonkey"
. "github.com/smartystreets/goconvey/convey"
"reflect"
"testing"
"utils"
)
func TestMaterialCenterTask(t *testing.T) {
Convey("test material task center", t, func() {
gomonkey.ApplyMethod(reflect.TypeOf(utils.MaterialUtilImpl{}), "QueryTaskByTaskId", func(_ utils.MaterialUtilImpl, ctx context.Context, centerTaskId int64) (resp *utils.MaterialTask, err error) {
return &utils.MaterialTask{
ErrCode: 0,
Msg: "mock调用成功",
}, nil
})
materialUtil := &utils.MaterialUtilImpl{}
resp, err := materialUtil.QueryTaskByTaskId(context.TODO(), 12345)
So(err, ShouldBeNil)
So(resp.Msg, ShouldEqual, "mock调用成功")
})
}
|