简介
golang单测,有一些约定,例如文件名是xxx.go,那么对应的测试文件就是xxx_test.go,单测的函数都需要是Test开头,然后使用go test命令,有时发现mock不住,一般都是内联(简短)函数mock失败,可以执行的时候加上编译条件禁止内联 -gcflags=all=-l
1. gomonkey
gomonkey用于mock跑单测,有以下的功能:
- 为函数打桩
- 为成员方法打桩
- 为全局变量打桩
- 为函数变量打桩
- 为函数打一个特定的桩序列
- 为成员方法打一个特定的桩序列
- 为函数变量打一个特定的桩序列
下面依次说说这几种方法的使用
1.1 使用
1.1.1 mock函数 ApplyFunc
// @param[in] target 被mock的函数
// @param[in] double 桩函数定义
// @retval patches 测试完成后,通过patches调用Reset删除桩
func ApplyFunc(target, double interface{}) *Patches
func (this *Patches) ApplyFunc(target, double interface{}) *Patches
桩函数的入参、返回值和要被mock的函数保持一致。
举个例子,例如现有调用链:logicFunc()-> netWorkFunc()
我们要测试logicFunc,而logicFunc里面调用了一个netWorkFunc,因为本地单测一般不会进行网络调用,所以我们要mock住netWorkFunc。
代码实例:
package main
import (
"fmt"
"testing"
"github.com/agiledragon/gomonkey"
"github.com/smartystreets/goconvey/convey"
)
func logicFunc(a,b int) (int, error) {
sum, err := netWorkFunc(a, b)
if err != nil {
return 0, err
}
return sum, nil
}
func netWorkFunc(a,b int) (int,error){
if a < 0 && b < 0 {
errmsg := "a<0 && b<0" //gomonkey有bug,函数一定要有栈分配变量,不然mock不住
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
}
func TestMockFunc(t *testing.T) {
convey.Convey("TestMockFunc1", t, func() {
var p1 = gomonkey.ApplyFunc(netWorkFunc, func(a, b int) (int, error) {
fmt.Println("in mock function")
return a+b, nil
})
defer p1.Reset()
sum, err := logicFunc(10, 20)
convey.So(sum, convey.ShouldEqual, 30)
convey.So(err, convey.ShouldBeNil)
})
}
直接用gomonkey.ApplyFunc,来mock netWorkFunc这个函数,然后调用logicFun,再用断言判断一致返回值是否符合预期。
这里用了convey包做断言,这本包断言挺丰富的,用起来很方便,也很简单:
convey.Convey("case的名字", t, func() {
具体测试case
convey.So(...) //断言
})
1.1.2 mock成员方法 ApplyMethod
method和function不同,实际上是属于类型的一部分,不像函数属于包的一部分,在函数地址的分配上会有所不同,因此不能直接用ApplyFunc去mock,这时就需要使用ApplyMethod了。
// @param[in] target 被mock的类型
// @param[in] methodName 要被mocket的函数名字,是个string
// @param[in] double 桩函数定义
// @retval patches 测试完成后,通过patches调用Reset删除桩
func ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
func (this *Patches) ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
下面的例子和上面ApplyFunc的差不多,也是logicFunc()-> netWorkFunc(),只不过从function变成了method,原理就是利用了reflect中的有几点要注意:
没办法mock unexported method。原因可以看reflect的原理。还有人论证为啥你永远不改测试unexported method:https://medium.com/@thrawn01/why-you-should-never-test-private-methods-f822358e010
类型T的method只包含receiver是T的;类型*T的method包含receiver是T和*T的。
写桩函数定义时,要把receiver写进去
例子:
type myType struct {
}
func (m *myType) logicFunc(a,b int) (int, error) {
sum, err := m.NetWorkFunc(a, b)
if err != nil {
return 0, err
}
return sum, nil
}
func (m *myType) NetWorkFunc(a,b int) (int,error){
if a < 0 && b < 0 {
errmsg := "a<0 && b<0"
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
}
func TestMockMethod(t *testing.T) {
Convey("TestMockMethod", t, func() {
var p *myType
fmt.Printf("method num:%d\n", reflect.TypeOf(p).NumMethod())
p1 := gomonkey.ApplyMethod(reflect.TypeOf(p), "NetWorkFunc", func(_ *myType, a,b int) (int,error) {
if a < 0 && b < 0 {
errmsg := "a<0 && b<0"
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
})
defer p1.Reset()
var m myType
sum, err := m.logicFunc(10, 20)
So(sum, ShouldEqual, 30)
So(err, ShouldBeNil)
})
}
1.1.3 mock全局变量 ApplyGlobalVar
// @param[in] target 全局变量的地址
// @param[in] double 全局变量的桩
func ApplyGlobalVar(target, double interface{}) *Patches
func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches
全局变量的mock很简单,直接看代码吧:
var num = 10
func TestApplyGlobalVar(t *testing.T) {
Convey("TestApplyGlobalVar", t, func() {
Convey("change", func() {
patches := ApplyGlobalVar(&num, 150)
defer patches.Reset()
So(num, ShouldEqual, 150)
})
Convey("recover", func() {
So(num, ShouldEqual, 10)
})
})
}
1.1.4 mock函数变量 ApplyFuncVar
// @param[in] target 函数变量的地址
// @param[in] double 桩函数的定义
func ApplyFuncVar(target, double interface{}) *Patches
func (this *Patches) ApplyFuncVar(target, double interface{}) *Patches
这个也很简单,直接看代码就明白了:
var funcVar = func(a,b int) (int,error) {
if a < 0 && b < 0 {
errmsg := "a<0 && b<0"
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
}
func TestMockFuncVar(t *testing.T) {
Convey("TestMockFuncVar", t, func() {
gomonkey.ApplyFuncVar(&funcVar, func(a,b int)(int,error) {
return a-b, nil
})
v, err := funcVar(20, 5)
So(v, ShouldEqual, 15)
So(err, ShouldBeNil)
})
}
1.1.5 mock函数序列 ApplyFuncSeq
有一种场景,被mock的函数,可能会被多次调用,我们希望按固定的顺序,然后每次调用的返回值都不一样,我们可以用一个全局变量记录这是第几次调用,然后桩函数里面做判断,更简洁的方法,就是用ApplyFuncSeq
type Params []interface{}
type OutputCell struct {
Values Params
Times int
}
// @param[in] target 要被mocket的函数
// @param[in] outputs 返回值
func ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches
func (this *Patches) ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches
其中Values是返回值,是一个[]interface{},对应实际可能有多个返回值。
看一下例子:
func getInt() (int) {
a := 1
fmt.Println("not in mock")
return a
}
func TestMockFuncSeq(t *testing.T) {
Convey("func seq", t, func() {
outputs := []gomonkey.OutputCell{
{Values:gomonkey.Params{2}, Times:1},
{Values:gomonkey.Params{1}, Times:0},
{Values:gomonkey.Params{3}, Times:2},
}
var p1 = gomonkey.ApplyFuncSeq(getInt, outputs)
defer p1.Reset()
So(getInt(), ShouldEqual, 2)
So(getInt(), ShouldEqual, 1)
So(getInt(), ShouldEqual, 3)
So(getInt(), ShouldEqual, 3)
})
}
注意:
- 对于Times,默认都是1次,填1次和0次其实都是1次
- 如果总共会调用N次,实际调用超过N次,那么会报错
1.1.6 mock成员方法序列 ApplyMethodSeq
同样的,既然有 ApplyFunSeq,那么就有 ApplyMethodSeq,基本都是一样的,不演示了
1.1.7 mock函数变量序列 ApplyFuncVarSeq
同样的,既然有 ApplyFunSeq,那么就有 ApplyFunVarSeq,基本都是一样的,不演示了