golang单元测试


简介

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中的有几点要注意:

  1. 没办法mock unexported method。原因可以看reflect的原理。还有人论证为啥你永远不改测试unexported method:https://medium.com/@thrawn01/why-you-should-never-test-private-methods-f822358e010

  2. 类型T的method只包含receiver是T的;类型*T的method包含receiver是T和*T的

  3. 写桩函数定义时,要把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)
    })
}

注意:

  1. 对于Times,默认都是1次,填1次和0次其实都是1次
  2. 如果总共会调用N次,实际调用超过N次,那么会报错

1.1.6 mock成员方法序列 ApplyMethodSeq

同样的,既然有 ApplyFunSeq,那么就有 ApplyMethodSeq,基本都是一样的,不演示了

1.1.7 mock函数变量序列 ApplyFuncVarSeq

同样的,既然有 ApplyFunSeq,那么就有 ApplyFunVarSeq,基本都是一样的,不演示了


发表评论

电子邮件地址不会被公开。 必填项已用*标注