1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# 单测定义: 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 也需要这么来定义,不是很方便 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 .使用 <pre class="lang:zsh decode:true " > 安装 - go get github.com/smartystreets/goconvey - go install github.com/smartystreets/goconvey 运行: ./goconvey.exe 页面访问: http://127.0.0.1:8080 </pre> 样例: <pre class="lang:go decode:true " > 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) }) }) } </pre> 5 .断言函数 <pre class="lang:go decode:true " > 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()) //不是以某时间间隔发生 </pre> 6 .Mock 方法 - ApplyFunc mock常规函数 <pre class="lang:go decode:true " > patches := ApplyFunc(GetCmdbInsts, func(dims *models.DimsInfo) ([]Endpoint, error) { return endpointList, nil }) defer patches.Reset() </pre> - ApplyMethod mock方法函数 <pre class="lang:go decode:true " > 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() </pre> - ApplyGlobalVar mock全局变量 <pre class="lang:go decode:true " > patches := ApplyGlobalVar(&num, 150) defer patches.Reset() </pre> - ApplyFuncSeq mock 函数序列桩 <pre class="lang:go decode:true " > 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) </pre> - ApplyFuncVar mock 函数变量 <pre class="lang:go decode:true " > patches := ApplyFuncVar(&fake.Marshal, func (_ interface{}) ([]byte, error) { return []byte(str), nil })// fake.Marshal是函数变量 defer patches.Reset() </pre> - ApplyFuncVarSeq 函数变量序列 <pre class="lang:go decode:true " > 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) </pre> - ApplyMethodSeq mock 成员方法打序列桩 <pre class="lang:go decode:true " > patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs) defer patches.Reset() output, err := e.Retrieve("") So(err, ShouldEqual, nil) So(output, ShouldEqual, info1) </pre> - mock 接口打桩,同接口打桩 <pre class="lang:go decode:true " > 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) </pre> 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 |