1 原则
- 单元测试文件名必须以 xxx_test.go 命名
- 方法必须是 TestXxx 开头,建议风格保持一致:驼峰,XXX标识需要测试的函数名
- 方法参数必须 t *testing.T
- 测试文件和被测试文件必须在一个包中
- 优先核心函数热点工具类函数
- 写明每个单测的注释,单测作用,比如:
测试用例 1:输入 4,输出 2。
测试用例 2:输入-1,输出 0。
2 框架使用
- GoConvey 和其他框架的兼容性较好,可直接在终端窗口和浏览器上使用,自带大量的标准断言函数,可以管理和运行测试用例
- goMonkey 在运行时通过汇编语句重写可执行文件,将待打桩函数或方法的实现跳转到桩实现,原理和热补丁类似。
通过 Monkey,我们可以解决函数或方法的打桩问题,但 Monkey 不是线程安全的,不要将 Monkey 用于并发的测试中
可以为全局变量、函数、过程、方法打桩,同时避免了gostub对代码的侵入
特性列表:
支持为一个函数打一个桩
支持为一个成员方法打一个桩
支持为一个接口打一个桩
支持为一个全局变量打一个桩
支持为一个函数变量打一个桩
支持为一个函数打一个特定的桩序列
支持为一个成员方法打一个特定的桩序列
支持为一个函数变量打一个特定的桩序列
支持为一个接口打一个特定的桩序列
缺陷:
对inline函数打桩无效
不支持多次调用桩函数(方法)而呈现不同行为的复杂情况
- GoMock 是由 Golang 官方开发维护的测试框架,实现了较为完整的基于 interface 的 Mock 功能,能够与 Golang 内置的 testing 包良好集成,也能用于其它的测试环境中。
GoMock 测试框架包含了 GoMock 包和 mockgen 工具两部分,其中 GoMock 包完成对桩对象生命周期的管理,mockgen 工具用来生成 interface 对应的 Mock 类源文件
缺陷:
只有以接口定义的方法才能mock,需要用mockgen生成源文件,然后用gomock去实现自己想要的数据,用法稍重。
- gostub 可以为全局变量、函数、过程打桩,比gomock轻量,不需要依赖接口
缺陷:
对项目源代码有侵入性,即被打桩方法必须赋值给一个变量,只有以这种形式定义的方法才能别打桩,gostub 由于方法的mock 还必须声明出 variable 才能进行mock,即使是 interface method 也需要这么来定义,不是很方便
- 外层框架——goconvey。
项目代码很多逻辑比较复杂,需要编写不同情况下的测试用例,用goconvey组织的测试代码逻辑层次比较清晰,有着较好的可读性和可维护性。断言方面感觉convey和testify功能差不多。不过convey没有testify社区活跃度高,后续使用convey时碰到一些问题,都不太容易找到解决办法
- 函数mock——gomonkey。项目代码基本都不是基于interface实现的,所以不太方便使用gomock,项目目前运行稳定,所以也不想因为单元测试重构原来的代码,所以也不太方便gostub,基本符合我们对函数打桩的需求。
- 持久层mock——sqlmock。我们持久层的框架是gorm。当时考虑2种方法进行mock,一种是使用gomonkey对gorm的函数进行mock,另一种则是选用sqlmock。如果使用gomonkey的话需要对连续调用的gorm函数都进行mock,过于繁杂。而用sqlmock的话只需匹配对应的sql语句即可
3. 安装
go get github.com/smartystreets/goconvey go install github.com/smartystreets/goconvey
运行:
./goconvey.exe
页面访问: http://127.0.0.1:8080
4. 样例
原程序
package goconvey import ( "errors" ) func Add(a, b int) int { return a + b } func Subtract(a, b int) int { return a - b } func Multiply(a, b int) int { return a * b } func Division(a, b int) (int, error) { if b == 0 { return 0, errors.New("被除数不能为 0") } return a / b, nil }
test文件
package goconvey import ( "testing" "github.com/smartystreets/goconvey/convey" ) func TestAdd(t *testing.T) { convey.Convey("将两数相加", t, func() { convey.So(Add(1, 2), convey.ShouldEqual, 3) }) } func TestSubtract(t *testing.T) { convey.Convey("将两数相减", t, func() { convey.So(Subtract(1, 2), convey.ShouldEqual, -1) }) } func TestMultiply(t *testing.T) { convey.Convey("将两数相乘", t, func() { convey.So(Multiply(3, 2), convey.ShouldEqual, 6) }) } func TestDivision(t *testing.T) { convey.Convey("将两数相除", t, func() { convey.Convey("除以非 0 数", func() { num, err := Division(10, 2) convey.So(err, convey.ShouldBeNil) convey.So(num, convey.ShouldEqual, 5) }) convey.Convey("除以 0", func() { _, err := Division(10, 0) convey.So(err, convey.ShouldNotBeNil) }) }) }
5 .断言函数
General Equality //通用比较
So(thing1, ShouldEqual, thing2) //相等
So(thing1, ShouldNotEqual, thing2) //不等
So(thing1, ShouldResemble, thing2) // a deep equals for arrays, slices, maps, and structs
So(thing1, ShouldNotResemble, thing2) //深度比较不相等
So(thing1, ShouldPointTo, thing2) //地址指向
So(thing1, ShouldNotPointTo, thing2) //地址不是指向
So(thing1, ShouldBeNil) //等于 nil
So(thing1, ShouldNotBeNil) //不等于 nil
So(thing1, ShouldBeTrue) //等于true
So(thing1, ShouldBeFalse) //等于false
So(thing1, ShouldBeZeroValue) //等于0值
Numeric Quantity comparison //数值比较
So(1, ShouldBeGreaterThan, 0) //大于
So(1, ShouldBeGreaterThanOrEqualTo, 0) //大于等于
So(1, ShouldBeLessThan, 2) //小于
So(1, ShouldBeLessThanOrEqualTo, 2) //小于等于
So(1.1, ShouldBeBetween, .8, 1.2) //区间内
So(1.1, ShouldNotBeBetween, 2, 3) //不在区间内
So(1.1, ShouldBeBetweenOrEqual, .9, 1.1) //区间取上下线
So(1.1, ShouldNotBeBetweenOrEqual, 1000, 2000) //不再区间
So(1.0, ShouldAlmostEqual, 0.99999999, .0001) // 容差比较,允许多的误差 tolerance is optional; default 0.0000000001
So(1.0, ShouldNotAlmostEqual, 0.9, .0001) //容差比较,不允许多少的误差
Collections //内建类型比较
So([]int{2, 4, 6}, ShouldContain, 4) //包含
So([]int{2, 4, 6}, ShouldNotContain, 5) //不包含
So(4, ShouldBeIn, ...[]int{2, 4, 6}) //在列表内
So(4, ShouldNotBeIn, ...[]int{1, 3, 5}) //不在列表内
So([]int{}, ShouldBeEmpty) //空列表
So([]int{1}, ShouldNotBeEmpty) //非空列表
So(map[string]string{"a": "b"}, ShouldContainKey, "a") //map 包含key
So(map[string]string{"a": "b"}, ShouldNotContainKey, "b") //map不包含key
So(map[string]string{"a": "b"}, ShouldNotBeEmpty) //非空map
So(map[string]string{}, ShouldBeEmpty) //空列表
So(map[string]string{"a": "b"}, ShouldHaveLength, 1) //长度 supports map, slice, chan, and string
Strings //字符串比较
So("asdf", ShouldStartWith, "as") //以某字符开头
So("asdf", ShouldNotStartWith, "df") //不是以某字符串开头
So("asdf", ShouldEndWith, "df") //以某字符串结尾
So("asdf", ShouldNotEndWith, "df") //不是以某字符串结尾
So("asdf", ShouldContainSubstring, "sd") //包含子串
So("asdf", ShouldNotContainSubstring, "er") //不包含子串
So("adsf", ShouldBeBlank) //空字符
So("asdf", ShouldNotBeBlank) //非空字符
panic //panic断言
So(func(), ShouldPanic) //发送panic
So(func(), ShouldNotPanic) //没有发生panic
So(func(), ShouldPanicWith, "") //以什么报错发什么 panic or errors.New("something")
So(func(), ShouldNotPanicWith, "") //不是以某错发生panic or errors.New("something")
Type checking //类型判断
So(1, ShouldHaveSameTypeAs, 0) //是否类型相同
So(1, ShouldNotHaveSameTypeAs, "asdf") //是否类型不相同
time.Time (and time.Duration) //时间判断
So(time.Now(), ShouldHappenBefore, time.Now()) //发生前
So(time.Now(), ShouldHappenOnOrBefore, time.Now()) //发生前或者当前时间
So(time.Now(), ShouldHappenAfter, time.Now()) //发生后
So(time.Now(), ShouldHappenOnOrAfter, time.Now()) //发生在之后或者当前时间
So(time.Now(), ShouldHappenBetween, time.Now(), time.Now()) //在某个时间区间
So(time.Now(), ShouldHappenOnOrBetween, time.Now(), time.Now()) //在区间内,并且取边界
So(time.Now(), ShouldNotHappenOnOrBetween, time.Now(), time.Now()) //不相等或者不再区间内
So(time.Now(), ShouldHappenWithin, duration, time.Now()) //以某个时间间隔固定发生
So(time.Now(), ShouldNotHappenWithin, duration, time.Now()) //不是以某时间间隔发生
6 .Mock 方法
// ApplyFunc mock常规函数 patches := ApplyFunc(GetCmdbInsts, func(dims *models.DimsInfo) ([]Endpoint, error) { return endpointList, nil }) defer patches.Reset() // ApplyMethod mock方法函数 var test *ConsistentHashRing patches.ApplyMethod(reflect.TypeOf(test), "GetNode", func(_ *ConsistentHashRing, pk string) (string, error) { return "", errors.New("get judge node fail") }) defer patches.Reset() // ApplyGlobalVar mock全局变量 patches := ApplyGlobalVar(&num, 150) defer patches.Reset() // ApplyFuncSeq mock 函数序列桩 patches := ApplyFuncSeq(fake.ReadLeaf, outputs) defer patches.Reset() output, err := fake.ReadLeaf("") if err != nil { log.Fatalf("Error reading leaf: %v", err) } So(output, ShouldEqual, info1) output, err = fake.ReadLeaf("") if err != nil { log.Fatalf("Error reading leaf: %v", err) } So(output, ShouldEqual, info2) // ApplyFuncVar mock 函数变量 patches := ApplyFuncVar(&fake.Marshal, func(_ interface{}) ([]byte, error) { return []byte(str), nil }) // fake.Marshal是函数变量 defer patches.Reset() // ApplyFuncVarSeq 函数变量序列 patches := ApplyFuncVarSeq(&fake.Marshal, outputs) defer patches.Reset() bytes, err := fake.Marshal("") if err != nil { log.Fatalf("Error marshaling: %v", err) } So(string(bytes), ShouldEqual, info1) bytes, err = fake.Marshal("") if err != nil { log.Fatalf("Error marshaling: %v", err) } So(string(bytes), ShouldEqual, info2) // ApplyMethodSeq mock 成员方法打序列桩 patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs) defer patches.Reset() output, err := e.Retrieve("") if err != nil { log.Fatalf("Error retrieving: %v", err) } So(output, ShouldEqual, info1) // mock 接口打桩,同接口打桩 e := &fake.Etcd{} info := "hello interface" patches.ApplyMethod(reflect.TypeOf(e), "Retrieve", func(_ *fake.Etcd, _ string) (string, error) { return info, nil }) output, err := db.Retrieve("") if err != nil { log.Fatalf("Error retrieving from db: %v", err) } So(output, ShouldEqual, info)
7 .参考链接
- https://mp.weixin.qq.com/s/eAptnygPQcQ5Ex8-6l0byA
- https://www.cnblogs.com/youhui/articles/11265947.html
- https://knapsackpro.com/testing_frameworks/difference_between/goconvey/vs/go-testify
- https://github.com/smartystreets/goconvey
- https://github.com/stretchr/testify/
- https://studygolang.com/topics/2992
- https://geektutu.com/post/quick-gomock.html gomock 的使用
- https://blog.marvel6.cn/2020/01/test-and-mock-db-by-xorm-with-the-help-of-convey-and-sqlmock/ 参考测试XORM
- https://github.com/dche423/dbtest/blob/master/pg/repository_test.go 参考测试gorm
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?