Go 如何进行单元测试

之前了解了 gomock 的一些基本使用技巧,但是对于单元测试这一 part 一直都是云里雾里,借着最近正好工作上要求写单测,学习一下 go 语言如何进行单元测试。

单元测试的含义

单元测试通常来讲是对代码中的某一个功能单元进行测试,这里的单元可能是一个函数,也可能对应实际业务需求的某一小块功能。通过单元测试,可以检测我们的代码在某一单元功能里存在的问题,减少 bug 的产生。

如何进行单元测试

这一章节主要介绍一下常用的测试工具包。

testing 包

go 自带的 testing 包可以很好的帮助我们进行单元测试,对于 testing 包的使用,参考之前记录的文章:<<Go 中的单元测试和基准测试的使用>>

在使用 testing 包的时候,有一个需要注意的地方就是,如果我们的测试文件由于调用了某一个函数功能在另一个包下,而这个包中的业务代码又调用了测试文件所在的包,就可能会导致循环引用。

这里的解决办法是可以对测试文件单独赋予一个 xxx_test 包名,将这个测试包变成外部测试包。一个测试文件的包声明后缀 _test 文件会被 go test 工具单独编译成一个包后被运行,所以带 _test 后缀的文件可以像一个单独的程序一样,自由导入其他的包,而不会出现循环导入。

这里需要注意的一种情况是,由于外部的测试包被看成了单独程序,所以这个时候它就没办法使用待测试源文件里面的非导出函数。这种问题的解决方法可以参考 go 的 fmt 包中的单元测试 fmt_test.go,其声明的包名是 package fmt_test,也就是一个外部测试包,其中 TestIsSpace 便是测试 fmt/scan.go 中的 isSpace 函数,其实现的方式是通过在 fmt/export_test.go 中添加这个函数的声明 (var IsSpace = isSpace) 来导出函数来实现的,其中 export_test.go 的包声明为 package fmt 。这样就巧妙的实现了只有测试包才能访问这个非导出函数的目的。

如果一个 *_test.go文件存在的唯一目的就在于此,并且自己不包含任何测试,它们一般称作 export_test.go

基于 testing 生成的文件测试文件都是基于 Table Driven 的测试设计模式生成的,我们可以在 TODO: add test cases 中添加我们的测试用例,一般包含输入输出结果,描述信息等。

如果测试用例过多,可以新建一个 testdata 目录存放我们的测试数据,go 对于命名为 testdata 的目录会自动忽略,具体例子可以参考 /usr/local/go/src/net/dnsclient_unix_test.go 中对于 hosts 文件的读取。

GoConvey

对于比较复杂的代码,比如说带有逻辑判断,嵌套等等,单独使用 testing 包,各种 t.Errorf 会显得比较混乱,可读性和可维护性都会比价差,所以对于多个分支层次的测试可以使用 goconvey 包。

GoConvey 包易于使用(只有Convey和So两个函数),与 testing 包可以直接配合,并且输出的测试结果更加可读,还提供了 WebUI 可视化页面,使用样例如下:

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
)
func TestXXXX(t *testing.T) {
      Convey("this is the first convey",t func() {
            x:=1
            Convey("this is the second convey",t,func(){
                  x++
                  Convey("this is the last convey",t,func(){
                        So(x,ShouldEqual,2)
                  })
            })
      })
}

每一个测试用例可以使用 Convey 函数进行包裹,convey 可以无限嵌套,用以体现测试用例之间的层级关系。使用 Convey 推荐的做法是:一个测试函数最外层只有一个 Convey,里面可以嵌套多层 Convey,同级的 Convey 表示不同条件下的测试场景。Convey 函数第一个参数为用例的描述,只有最外层的Convey第二个参数是测试函数的入参 *testing.T,第三个参数为用例判断函数(习惯使用闭包),最后通过 So 函数完成断言判断即可。

So 函数第一个参数为输入的值,第二个参数为断言条件,第三个参数为期望值,goconvey 包定义了大部分的基础断言条件,如果有需要,也可以自己定义。注意:层级相同的子 Convey 的执行策略是并行的,但是一个 Convey 下的子 So 执行是串行的

Web 界面

GoConvey 不仅支持在命令行进行自动化编译测试,而且还支持在 Web 界面进行自动化编译测试。想要使用 GoConvey 的 Web 界面特性,需要在测试文件所在目录下执行 goconvey:

goconvey

在 Web 界面中可以使用以下功能:

  1. 可以设置界面主题
  2. 查看完整的测试结果
  3. 使用浏览器提醒等实用功能
  4. 自动检测代码变动并编译测试
  5. 半自动化书写测试用例
  6. 查看测试覆盖率
  7. 临时屏蔽某个包的编译测试

Skip

针对想忽略但又不想删掉或注释掉某些断言操作,GoConvey 提供了 Convey/SoSkip 方法:

  • SkipConvey 函数表明相应的闭包函数将不被执行

  • SkipSo 函数表明相应的断言将不被执行

当存在 SkipConveySkipSo 时,测试日志中会显式打上 “skipped” 形式的标记:

  • 当测试代码中存在 SkipConvey 时,相应闭包函数中不管是否为 SkipSo,都将被忽略,测试日志中对应的符号仅为一个”⚠”
  • 当测试代码 Convey 语句中存在 SkipSo 时,测试日志中每个 So 对应一个”✔”或”✘”,每个 SkipSo 对应一个”⚠”,按实际顺序排列
  • 不管存在 SkipConvey 还是 SkipSo 时,测试日志中都有字符串 "{n} total assertions (one or more sections skipped)",其中 {n} 表示测试中实际已运行的断言语句数。

对于原生的 testing 包生成的测试用例,使用的都是静态数据组成的数组,而实际的业务场景中,测试的函数往往还会调用其他的函数/结构体方法/接口等等,还有可能会使用到全局变量,这时候就需要对这些函数/结构体方法/接口/全局变量等进行 mock,也就是打桩。

GoStub

GoStub 主要用来对全局变量、函数进行打桩。在日常使用中,主要用来对全局变量打桩

对全局变量打桩示例如下:

stubs := gostub.Stub(&num, 150)
defer stubs.Reset()

stubs 是 GoStub 框架的函数接口 Stub 返回的对象,该对象有 Reset 操作,即将全局变量的值恢复为原值。

gomonkey & monkey

对于函数以及结构体成员方法,我们一般会使用 gomonkey/monkey 来进行 mock。

Monkey

Monkey是Golang的一个猴子补丁(monkeypatching)框架,其通过运行时用汇编语句修改跳转的函数地址,来实现对一个函数/结构体成员方法的 mock。

对函数的 Mock

因为这个工具包简单易用,这里直接贴 demo 代码:

import (
    "testing"
    . "github.com/smartystreets/goconvey/convey"
    . "github.com/bouk/monkey"
    "infra/osencap"
)

const any = "any"

func TestExec(t *testing.T) {
    Convey("test has digit", t, func() {
        Convey("for succ", func() {
            outputExpect := "xxx-vethName100-yyy"
            guard := Patch(osencap.Exec, func(_ string, _ ...string) (string, error) {
                return outputExpect, nil
            })
            defer guard.Unpatch()
            output, err := osencap.Exec(any, any)
            So(output, ShouldEqual, outputExpect)
            So(err, ShouldBeNil)
        })
    })
}

monkey.Patch(<target function>, <replacement function>) 函数可以实现对目的函数的替换,使用monkey.Unpatch(<target function>) 来恢复目的函数。

对结构体成员方法的 Mock

var e *Etcd
guard := PatchInstanceMethod(reflect.TypeOf(e), "Get", func(_ *Etcd, _ string) []string {
    return []string{"task1", "task5", "task8"}
})
defer guard.Unpatch()

调用 monkey.PatchInstanceMethod(<type>, <name>, <replacement>) 函数可以实现对结构体成员方法的替换:

  • 第一个参数是 reflect.TypeOf(实例)
  • 第二个参数是方法名
  • 第三个参数把方法绑定到类型上
  • 使用 monkey.UnpatchInstanceMethod(<type>, <name>)来恢复目的函数。

在使用 monkey 时需要注意的是, monkey 包不能在函数内联的情形下patch,所以使用 go test -gcflags=all=-l选项关闭内联。Monkey 中的函数也不是线程安全的。另外 Patch 和 PatchInstanceMethod 返回 *monkey.PatchGuard 类型变量,可以直接调用其 Unpatch 方法来恢复目的函数,也可以调用 monkey.UnpatchAll 来恢复所有 mockeypathches。

gomonkey

gomonkey 是一款覆盖范围特别广的打桩框架,其支持的打桩类型有:

  • 支持为一个函数打一个桩
  • 支持为一个成员方法打一个桩
  • 支持为一个全局变量打一个桩
  • 支持为一个函数变量打一个桩
  • 支持为一个函数打一个特定的桩序列
  • 支持为一个成员方法打一个特定的桩序列
  • 支持为一个函数变量打一个特定的桩序列

同样的,gomonkey 在内联的情况下也无法使用,同样需要添加参数 go test -gcflags=all=-l 来关闭内联

gomonkey 中定义的各种方法如下:

ApplyFunc 对外部函数 mock

ApplyFunc 第一个参数是函数名,第二个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

import (
    "encoding/json"
    "testing"

    . "github.com/agiledragon/gomonkey/v2"
    "github.com/agiledragon/gomonkey/v2/test/fake"
    . "github.com/smartystreets/goconvey/convey"
)

var (
    outputExpect = "xxx-vethName100-yyy"
)

func TestApplyFunc(t *testing.T) {
    Convey("TestApplyFunc", t, func() {

        Convey("one func for succ", func() {
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                return outputExpect, nil
            })
            defer patches.Reset()
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)
        })

        Convey("one func for fail", func() {
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                return "", fake.ErrActual
            })
            defer patches.Reset()
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, fake.ErrActual)
            So(output, ShouldEqual, "")
        })

        Convey("two funcs", func() {
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                return outputExpect, nil
            })
            defer patches.Reset()
            patches.ApplyFunc(fake.Belong, func(_ string, _ []string) bool {
                return true
            })
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)
            flag := fake.Belong("", nil)
            So(flag, ShouldBeTrue)
        })

        Convey("input and output param", func() {
            patches := ApplyFunc(json.Unmarshal, func(data []byte, v interface{}) error {
                if data == nil {
                    panic("input param is nil!")
                }
                p := v.(*map[int]int)
                *p = make(map[int]int)
                (*p)[1] = 2
                (*p)[2] = 4
                return nil
            })
            defer patches.Reset()
            var m map[int]int
            err := json.Unmarshal([]byte("123"), &m)
            So(err, ShouldEqual, nil)
            So(m[1], ShouldEqual, 2)
            So(m[2], ShouldEqual, 4)
        })
    })
}

ApplyMethod 对结构体方法 mock

ApplyMethod 第一个参数是目标类的指针变量的反射类型,第二个参数是字符串形式的方法名,第三个参数是桩函数。

测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

使用 ApplyMethod 可以对非私有方法进行 mock,如果是私有的方法,gomonkey 通过反射是找不到的。

func TestApplyMethod(t *testing.T) {
    slice := fake.NewSlice()
    var s *fake.Slice
    Convey("TestApplyMethod", t, func() {

        Convey("for succ", func() {
            err := slice.Add(1)
            So(err, ShouldEqual, nil)
            patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
                return nil
            })
            defer patches.Reset()
            err = slice.Add(1)
            So(err, ShouldEqual, nil)
            err = slice.Remove(1)
            So(err, ShouldEqual, nil)
            So(len(slice), ShouldEqual, 0)
        })

        Convey("for already exist", func() {
            err := slice.Add(2)
            So(err, ShouldEqual, nil)
            patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
                return fake.ErrElemExsit
            })
            defer patches.Reset()
            err = slice.Add(1)
            So(err, ShouldEqual, fake.ErrElemExsit)
            err = slice.Remove(2)
            So(err, ShouldEqual, nil)
            So(len(slice), ShouldEqual, 0)
        })

        Convey("two methods", func() {
            err := slice.Add(3)
            So(err, ShouldEqual, nil)
            defer slice.Remove(3)
            patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
                return fake.ErrElemExsit
            })
            defer patches.Reset()
            patches.ApplyMethod(reflect.TypeOf(s), "Remove", func(_ *fake.Slice, _ int) error {
                return fake.ErrElemNotExsit
            })
            err = slice.Add(2)
            So(err, ShouldEqual, fake.ErrElemExsit)
            err = slice.Remove(1)
            So(err, ShouldEqual, fake.ErrElemNotExsit)
            So(len(slice), ShouldEqual, 1)
            So(slice[0], ShouldEqual, 3)
        })

        Convey("one func and one method", func() {
            err := slice.Add(4)
            So(err, ShouldEqual, nil)
            defer slice.Remove(4)
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                return outputExpect, nil
            })
            defer patches.Reset()
            patches.ApplyMethod(reflect.TypeOf(s), "Remove", func(_ *fake.Slice, _ int) error {
                return fake.ErrElemNotExsit
            })
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)
            err = slice.Remove(1)
            So(err, ShouldEqual, fake.ErrElemNotExsit)
            So(len(slice), ShouldEqual, 1)
            So(slice[0], ShouldEqual, 4)
        })
    })
}

ApplyGlobalVar 对全局变量 mock

ApplyGlobalVar 第一个参数是全局变量的地址,第二个参数是全局变量的桩。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

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)
        })
    })
}

ApplyFuncVar 对函数变量 mock

ApplyFuncVar 第一个参数是函数变量的地址,第二个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

假设函数变量如下:

var Marshal = func(v interface{}) ([]byte, error) {
    return nil, nil
}
func TestApplyFuncVar(t *testing.T) {
    Convey("TestApplyFuncVar", t, func() {

        Convey("for succ", func() {
            str := "hello"
            patches := ApplyFuncVar(&fake.Marshal, func(_ interface{}) ([]byte, error) {
                return []byte(str), nil
            })
            defer patches.Reset()
            bytes, err := fake.Marshal(nil)
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, str)
        })

        Convey("for fail", func() {
            patches := ApplyFuncVar(&fake.Marshal, func(_ interface{}) ([]byte, error) {
                return nil, fake.ErrActual
            })
            defer patches.Reset()
            _, err := fake.Marshal(nil)
            So(err, ShouldEqual, fake.ErrActual)
        })
    })
}

ApplyFuncSeq 构造多个测试用例对外部函数 mock

ApplyFuncSeq 第一个参数是函数名,第二个参数是特定的桩序列参数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

其中,第二个参数 OutputCell 的定义为:

type Params []interface{}
type OutputCell struct {
    Values Params
    Times  int // 生效次数
}
func TestApplyFuncSeq(t *testing.T) {
    Convey("TestApplyFuncSeq", t, func() {

        Convey("default times is 1", func() {
            info1 := "hello cpp"
            info2 := "hello golang"
            info3 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}},
                {Values: Params{info2, nil}},
                {Values: Params{info3, nil}},
            }
            patches := ApplyFuncSeq(fake.ReadLeaf, outputs)
            defer patches.Reset()

            runtime.GC()

            output, err := fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info2)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info3)
        })

        Convey("retry succ util the third times", func() {
            info1 := "hello cpp"
            outputs := []OutputCell{
                {Values: Params{"", fake.ErrActual}, Times: 2},
                {Values: Params{info1, nil}},
            }
            patches := ApplyFuncSeq(fake.ReadLeaf, outputs)
            defer patches.Reset()
            output, err := fake.ReadLeaf("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
        })

        Convey("batch operations failed on the third time", func() {
            info1 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}, Times: 2},
                {Values: Params{"", fake.ErrActual}},
            }
            patches := ApplyFuncSeq(fake.ReadLeaf, outputs)
            defer patches.Reset()
            output, err := fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, fake.ErrActual)
        })

    })
}

ApplyMethodSeq 构造多个测试用例对结构体方法 mock

ApplyMethodSeq 第一个参数是目标类的指针变量的反射类型,第二个参数是字符串形式的方法名,第三参数是特定的桩序列参数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

func TestApplyMethodSeq(t *testing.T) {
    e := &fake.Etcd{}
    Convey("TestApplyMethodSeq", t, func() {

        Convey("default times is 1", func() {
            info1 := "hello cpp"
            info2 := "hello golang"
            info3 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}},
                {Values: Params{info2, nil}},
                {Values: Params{info3, nil}},
            }
            patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
            defer patches.Reset()
            output, err := e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info2)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info3)
        })

        Convey("retry succ util the third times", func() {
            info1 := "hello cpp"
            outputs := []OutputCell{
                {Values: Params{"", fake.ErrActual}, Times: 2},
                {Values: Params{info1, nil}},
            }
            patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
            defer patches.Reset()
            output, err := e.Retrieve("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
        })

        Convey("batch operations failed on the third time", func() {
            info1 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}, Times: 2},
                {Values: Params{"", fake.ErrActual}},
            }
            patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
            defer patches.Reset()
            output, err := e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, fake.ErrActual)
        })

    })
}

ApplyFuncVarSeq 构造多个测试用例对函数变量 mock

ApplyFuncVarSeq 第一个参数是函数变量地址,第二个参数是特定的桩序列参数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

func TestApplyFuncVarSeq(t *testing.T) {
    Convey("TestApplyFuncVarSeq", t, func() {

        Convey("default times is 1", func() {
            info1 := "hello cpp"
            info2 := "hello golang"
            info3 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{[]byte(info1), nil}},
                {Values: Params{[]byte(info2), nil}},
                {Values: Params{[]byte(info3), nil}},
            }
            patches := ApplyFuncVarSeq(&fake.Marshal, outputs)
            defer patches.Reset()
            bytes, err := fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info2)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info3)
        })

        Convey("retry succ util the third times", func() {
            info1 := "hello cpp"
            outputs := []OutputCell{
                {Values: Params{[]byte(""), fake.ErrActual}, Times: 2},
                {Values: Params{[]byte(info1), nil}},
            }
            patches := ApplyFuncVarSeq(&fake.Marshal, outputs)
            defer patches.Reset()
            bytes, err := fake.Marshal("")
            So(err, ShouldEqual, fake.ErrActual)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, fake.ErrActual)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
        })

        Convey("batch operations failed on the third time", func() {
            info1 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{[]byte(info1), nil}, Times: 2},
                {Values: Params{[]byte(""), fake.ErrActual}},
            }
            patches := ApplyFuncVarSeq(&fake.Marshal, outputs)
            defer patches.Reset()
            bytes, err := fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, fake.ErrActual)
        })

    })
}

NewPatches 用于表驱动场景

NewPatches 是 patches 对象的显式构造函数,一般用于目标和桩的表驱动场景。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

func TestPatchPair(t *testing.T) {

    Convey("TestPatchPair", t, func() {

        Convey("TestPatchPair", func() {
            patchPairs := [][2]interface{}{
                {
                    fake.Exec,
                    func(_ string, _ ...string) (string, error) {
                        return outputExpect, nil
                    },
                },
                {
                    json.Unmarshal,
                    func(_ []byte, v interface{}) error {
                        p := v.(*map[int]int)
                        *p = make(map[int]int)
                        (*p)[1] = 2
                        (*p)[2] = 4
                        return nil
                    },
                },
            }
            patches := NewPatches()
            defer patches.Reset()
            for _, pair := range patchPairs {
                patches.ApplyFunc(pair[0], pair[1])
            }

            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)

            var m map[int]int
            err = json.Unmarshal(nil, &m)
            So(err, ShouldEqual, nil)
            So(m[1], ShouldEqual, 2)
            So(m[2], ShouldEqual, 4)
        })

    })
}

注意事项

使用 gomonkey 时还有一个需要注意的点就是,必须找到该方法对应的真实的结构体的方法,而不能因为嵌套了内部结构体就直接只用外部结构体来进行反射,这会导致 mock 失败。

另外,使用 gomonkey 同样需要通过 go test -gcflags=all=-l 选项关闭内联。

gomock

对于接口方法,则使用 gomock 来进行 mock,go mock 是官方自带的接口gomock包,这个包完成对桩对象生命周期的管理,GoMock 还包含一个 mockgen 命令行工具,用来自动生成 interface 对应的 Mock 类源文件,具体使用见gomock 使用

使用总结

  1. 普通的单测使用 testing 包足矣,而当需要模拟全局变量的时候,使用 GoStub 是最方便快捷的方式。
  2. 多条件测试可以使用 convey 进行层级划分,方便定位问题和提升代码阅读体验。
  3. 对于函数,成员方法的 mock 可以使用 monkey 或者 gomonkey。
  4. 对于接口的 mock 使用 gomock

查看单测覆盖率

通过以下命令可以将单测的覆盖情况:

go test -count=1 -v -coverprofile=c.out ./... -gcflags=all=-l && go tool cover -html=c.out && rm c.out

参考文章

GoConvey框架使用指南

GoStub框架使用指南

Monkey框架使用指南

gomonkey-test

gomonkey 1.0 正式发布!