简介
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{}) *Patchesfunc (this *Patches) ApplyFunc(target, double interface{}) *Patches
桩函数的入参、返回值和要被mock的函数保持一致。
举个例子,例如现有调用链:logicFunc()-> netWorkFunc()
我们要测试logicFunc,而logicFunc里面调用了一个netWorkFunc,因为本地单测一般不会进行网络调用,所以我们要mock住netWorkFunc。
代码实例:
package mainimport ( "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{}) *Patchesfunc (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{}) *Patchesfunc (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches
全局变量的mock很简单,直接看代码吧:
var num = 10func 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{}) *Patchesfunc (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) *Patchesfunc (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,基本都是一样的,不演示了
文章来源:智云一二三科技
文章标题:golang单元测试
文章地址:https://www.zhihuclub.com/7277.shtml