人生三从境界:昨夜西风凋碧树,独上高楼,望尽天涯路。 衣带渐宽终不悔,为伊消得人憔悴。 众里寻他千百度,蓦然回首,那人却在灯火阑珊处。

 

1 .原则:

- 单元测试文件名必须以 xxx_test.go 命名
- 方法必须是 TestXxx 开头,建议风格保持一致:驼峰,XXX标识需要测试的函数名
- 方法参数必须 t *testing.T
- 测试文件和被测试文件必须在一个包中
- 优先核心函数热点工具类函数
- 写明每个单测的注释,单测作用,比如:
测试用例 1:输入 4,输出 2。
测试用例 2:输入-1,输出 02 .框架使用
- 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 也需要这么来定义,不是很方便

 

3 .使用goconvey+gomonkey进行测试

- 外层框架——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语句即可

4 .安装
- go get github.com/smartystreets/goconvey
- go install github.com/smartystreets/goconvey

运行:

./goconvey.exe

页面访问: http://127.0.0.1:8080


样例:
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
}


package goconvey

import (
"testing"

. "github.com/smartystreets/goconvey/convey"
)

func TestAdd(t *testing.T) {
Convey("将两数相加", t, func() {
So(Add(1, 2), ShouldEqual, 3)
})
}

func TestSubtract(t *testing.T) {
Convey("将两数相减", t, func() {
So(Subtract(1, 2), ShouldEqual, -1)
})
}

func TestMultiply(t *testing.T) {
Convey("将两数相乘", t, func() {
So(Multiply(3, 2), ShouldEqual, 6)
})
}

func TestDivision(t *testing.T) {
Convey("将两数相除", t, func() {

//patch

Convey("除以非 0 数", func() {
num, err := Division(10, 2)
So(err, ShouldBeNil)
So(num, ShouldEqual, 5)
})

Convey("除以 0", func() {
_, err := Division(10, 0)
So(err, 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("") So(err, ShouldEqual, nil) So(output, ShouldEqual, info1) output, err = fake.ReadLeaf("") So(err, ShouldEqual, nil) 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("") So(err, ShouldEqual, nil) So(string(bytes), ShouldEqual, info1) bytes, err = fake.Marshal("") So(err, ShouldEqual, nil) So(string(bytes), ShouldEqual, info2) - ApplyMethodSeq mock 成员方法打序列桩 patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs) defer patches.Reset() output, err := e.Retrieve("") So(err, ShouldEqual, nil) 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("") So(err, ShouldEqual, nil) 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

 

posted on 2022-06-13 15:58  测试开发喵  阅读(579)  评论(0编辑  收藏  举报