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,基本都是一样的,不演示了

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342