go 单元测试写法
参考 https://studygolang.com/articles/22982
testing
go语言package提供的自动化测试框架,
testing支持普通用例测试、压力测试、并行测试,基本能满足单测需求;
缺失:
testing不支持mock数据,想要mock数据必须自己实现mock逻辑,这会增加许多不必要的工作量。
mock的重要性
理想情况下,一个好的函数结构必须有入参、出参,完成的是一项单一、独立的工作,比如:
func add(a int, b int) int {
return a + b
}
这样的函数写单测很简单,且轻易就能达到100%覆盖率。
但实际工作中,这样纯粹的函数占比可能不到10%,单个函数往往不是独立的,它需要依赖于其他模块、三方库、数据库等等。
以上传图片为例,上传一张图片样本函数:
func uploadImage(content []byte, UID string, fdfs *tsFDFS) error {
// 1. 上传图片到fdfs
url := fdfs.Upload(content)
// 2. 生成缩略图
resize.Resize(…)
jpeg.Encode(…)
// 3. 图片入库
sample := &database.yangbenModel{ … }
database.CreateSample(…)
// 4. 图片绑定任务
binding := &database.ExamplennotationTask{ … }
database.CreateBinding(…)
return nil
}
如果在单测中想要正常走完这个函数且不出错,我们需要有一个真实的fdfs环境可以上传数据成功,一个真实的数据库,且根据业务逻辑,我们想要插入一个sample前,必须有对应的enterprise、pipeline、annotation-task等等;
于是,在此前,项目中使用testing的单测实现思路是这样的:
func TestUploadImage(t *testing.T) {
// 1. 创建真实的fdfs环境
fdfs := &FDFS{ … }
// 2. 连接真实的数据库
db := database.NewGormConn(…)
// 3. 插入一条enters数据
enters := &database.EnterpriseModel{…}
database.CreateEnters(…)
// 4. 插入一条业务数据
…
// 5. 插入另外一条业务数据
…
// 6. 开始执行单测
uploadImage(…)
…
}
可以看到,单测之前进行了大量耗时的、冗余的、无意义的工作
单测执行耗时长,正常一条单测时间一般在100ms内,在模拟了真实场景后,单测时间大幅增加,甚至项目中有部分单测超过1分钟;
单测编码效率低,需要了解了业务场景后,才能编写完一条单测case;
过于依赖真实环境,稳定性差,维护成本高,对于一些无法模拟真实环境的流程,只能放弃覆盖测试。
mock框架
gomonkey 单元测试
安装
go get -u -v https://github.com/agiledragon/gomonkey
github地址为:https://github.com/agiledragon/gomonkey
参考博客: https://studygolang.com/articles/15034
目前自己尝试过mock方法 把代码使用的粘贴供gopher参考下
这个地方是rpc调用其他服务,
测试中用打桩的方式,将这个
函数的结果先执行了,当执行
这个函数时,采用这个函数
打桩时mock的数据就可以了
测试文件中如何写mock第三方数据呢?
ApplyMethod 第三个参数
func() 中的第一个参数为这个
方法属于那个类型,剩余的参数
书写方式为 func(_ 绑定的那个类型,
,_ type)
例如
func(_ 绑定的那个类型,
,_ string, _ int)(返回结果)
gostub
对全局变量、函数或过程打桩,对代码中的外部依赖进行劫持并替换成mock的内容
为一个全局变量打桩
stubs := gostub.Stub(&num, 10)
defer stubs.Reset()
为一个函数打桩
stubs := gostub.StubFunc(&Func, func(args string) error {
return nil
})
defer stubs.Reset()
为一个过程打桩
stubs := gostub.StubFunc(&Destroy)
defer stubs.Reset()
场景组合
以上文测试上传一张图片样本为例
func TestUploadImage(t *testing.T) {
// 1. stub fdfs
stubs := gostub.StubFunc(&Upload, func() string {
return “”
})
defer stubs.Reset()
// 2. stub 生成缩略图
stubs = gostub.StubFunc(&resize.Resize, func() error {
return nil
})
// 3. stub sample create
stubs = gostub.StubFunc(&database.CreateSample, func() error {
return nil
})
...
// 4. stub binding create
...
// 5. 开始执行单测
uploadImage(...)
...
}
缺点
对代码有一定的侵入性 (需要将函数改为全局变量语法形式、对三方库需要做一层包装)
无法对方法(成员函数)进行打桩
monkey
monkey是go的一个猴子补丁(monkeypatching)框架,在运行时通过汇编语句重写可执行文件,将待打桩函数或方法的实现跳转到桩实现,原理和热补丁类似。
通过monkey可以解决stub无法为方法打桩的问题,但monkey不是线程安全的,不能用于并发测试。
为一个函数打桩
guard := monkey.Patch(Func, func(args string) error {
return nil
})
defer guard.Unpatch()
为一个方法打桩
guard := monkey.PatchInstanceMethod(reflect.TypeOf(client), “Test”, func(args string) error {
return nil
})
defer guard.Unpatch()
缺点
线程不安全
在macOS 10.15及以上版本无法运行,由于其原理是修改可执行文件,从Catalina版本起,系统对文件读写权限做了更严格的管理,monkey修改的文件无写权限,运行panic,参考https://github.com/agiledragon/gomonkey/issues/10