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 界面中可以使用以下功能:
- 可以设置界面主题
- 查看完整的测试结果
- 使用浏览器提醒等实用功能
- 自动检测代码变动并编译测试
- 半自动化书写测试用例
- 查看测试覆盖率
- 临时屏蔽某个包的编译测试
Skip
针对想忽略但又不想删掉或注释掉某些断言操作,GoConvey 提供了 Convey/So
的 Skip
方法:
SkipConvey
函数表明相应的闭包函数将不被执行SkipSo
函数表明相应的断言将不被执行
当存在 SkipConvey
或 SkipSo
时,测试日志中会显式打上 “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 使用
使用总结
- 普通的单测使用 testing 包足矣,而当需要模拟全局变量的时候,使用 GoStub 是最方便快捷的方式。
- 多条件测试可以使用 convey 进行层级划分,方便定位问题和提升代码阅读体验。
- 对于函数,成员方法的 mock 可以使用 monkey 或者 gomonkey。
- 对于接口的 mock 使用 gomock
查看单测覆盖率
通过以下命令可以将单测的覆盖情况:
go test -count=1 -v -coverprofile=c.out ./... -gcflags=all=-l && go tool cover -html=c.out && rm c.out
参考文章: