github.com/tencent/goom@v1.0.1/README.md (about) 1 # GOOM单测Mock框架 2 ## 介绍 3 ### 背景 4 1. 基于公司目前内部没有一款自己维护的适合公司业务迭代速度和稳定性要求的mock框架,众多项目采用外界开源的gomonkey框架进行函数的mock,因其存在较一些的bug,不支持异包未导出函数mock,同包未导出方法mock等等问题, 加上团队目前实现一款改进版-无需unpath即可在mock过程中调用原函数的特性,可以支持到延迟模拟,参数更改,mock数据录制等功能,因此建立此项目 5 2. 目前有一半以上方案是基于gomock类似的实现方案, 此mock方案需要要求业务代码具备良好的接口设计,才能顺利生成mock代码,而goom只需要指定函数名称或函数定义,就能支持到任意函数的mock,任意函数异常注入,延时模拟等扩展性功能 6 7 ### 功能特性 8 1. mock过程中调用原函数(线程安全, 支持并发单测) 9 2. 异常注入,对函数调用支持异常注入,延迟模拟等稳定性测试 10 3. 所有操作都是并发安全的 11 4. 未导出(未导出)函数(或方法)的mock(不建议使用, 对于未导出函数的Mock 通常都是因为代码设计可能有问题, 此功能会在未来版本中废弃) 12 5. 支持M1 mac环境运行,支持IDE debug,函数、方法mock,接口mock,未导出函数mock,等能力均可在arm64架构上使用 13 14 ### 将来 15 1. 支持数据驱动测试 16 2. 支持Mock锚点定义 17 3. 支持代码重构 18 19 ## 注意!!!不要过度依赖mock 20 21 > [1.千万不要过度依赖于mock](https://mp.weixin.qq.com/s?__biz=MzA5MTAzNjU1OQ==&mid=2454780683&idx=1&sn=aabc85f3bd2cfa21b8b806bad581f0c5) 22 > 23 > 2.对于正规的第三方库,比如mysql、gorm的库本身会提供mock能力, 可参考[sql_test.go](https://github.com/Jakegogo/goom_best_practices/blob/master/example/sql_test.go) 24 > 25 > 3.对于自建的内部依赖库, 建议由库的提供方编写mock(1.使用方无需关心提供方的实现细节、2.由库提供方负责版本升级时mock实现逻辑的更新) 26 27 ## Install 28 ```bash 29 # 支持的golang版本: go1.11-go1.18 30 go get github.com/tencent/goom 31 ``` 32 33 ## Tips 34 ``` 35 注意: 按照go编译规则,短函数会被内联优化,导致无法mock的情况,编译参数需要加上 -gcflags=all=-l 关闭内联 36 例如: go test -gcflags=all=-l hello.go 37 ``` 38 39 ## Getting Start 40 ```golang 41 // 在需要使用mock的测试文件import 42 import "github.com/tencent/goom" 43 ``` 44 ### 1. 基本使用 45 #### 1.1. 函数mock 46 ```golang 47 // foo 函数定义如下 48 func foo(i int) int { 49 //... 50 return 0 51 } 52 53 // mock示例 54 // 创建当前包的mocker 55 mock := mocker.Create() 56 57 // mock函数foo并设定返回值为1 58 mock.Func(foo).Return(1) 59 s.Equal(1, foo(0), "return result check") 60 61 // 可搭配When使用: 参数匹配时返回指定值 62 mock.Func(foo).When(1).Return(2) 63 s.Equal(2, foo(1), "when result check") 64 65 // 使用arg.In表达式,当参数为1、2时返回值为100 66 mock.Func(foo).When(arg.In(1, 2)).Return(100) 67 s.Equal(100, foo(1), "when in result check") 68 s.Equal(100, foo(2), "when in result check") 69 70 // 按顺序依次返回(等价于gomonkey的Sequence) 71 mock.Func(foo).Returns(1, 2, 3) 72 s.Equal(1, foo(0), "returns result check") 73 s.Equal(2, foo(0), "returns result check") 74 s.Equal(3, foo(0), "returns result check") 75 76 // mock函数foo,使用Apply方法设置回调函数 77 // 注意: Apply和直接使用Return都可以实现mock,两种方式二选一即可 78 // Apply可以在桩函数内部实现自己的逻辑,比如根据不同参数返回不同值等等。 79 mock.Func(foo).Apply(func(int) int { 80 return 1 81 }) 82 s.Equal(1, foo(0), "apply callback check") 83 84 85 // bar 多参数函数 86 func bar(i interface{}, j int) int { 87 //... 88 return 0 89 } 90 91 // 忽略第一个参数, 当第二个参数为1、2时返回值为100 92 mock.Func(bar).When(arg.Any(), arg.In(1, 2)).Return(100) 93 s.Equal(100, bar(-1, 1), "any param result check") 94 s.Equal(100, bar(0, 1), "any param result check") 95 s.Equal(100, bar(1, 2), "any param result check") 96 s.Equal(100, bar(999, 2), "any param result check") 97 ``` 98 99 #### 1.2. 结构体方法mock 100 ```golang 101 // 结构体定义如下 102 type Struct1 struct{ 103 } 104 105 // Call 导出方法 106 func (f *Struct1) Call(i int) int { 107 return i 108 } 109 110 // mock示例 111 // 创建当前包的mocker 112 mock := mocker.Create() 113 114 // mock 结构体Struct1的方法Call并设置其回调函数 115 // 注意: 当使用Apply方法时,如果被mock对象为结构体方法, 那么Apply参数func()的第一个参数必须为接收体(即结构体/指针类型) 116 // 其中, func (f *Struct1) Call(i int) int 和 &Struct1{} 与 _ *Struct1同时都是带指针的接受体类型, 需要保持一致 117 mock.Struct(&Struct1{}).Method("Call").Apply(func(_ *Struct1, i int) int { 118 return i * 2 119 }) 120 121 // mock 结构体struct1的方法Call并返回1 122 // 简易写法直接Return方法的返回值, 无需关心方法签名 123 mock.Struct(&Struct1{}).Method("Call").Return(1) 124 ``` 125 126 #### 1.3. 结构体的未导出方法mock 127 ```golang 128 129 // call 未导出方法示例 130 func (f *Struct1) call(i int) int { 131 return i 132 } 133 134 // mock 结构体Struct1的未导出方法call, mock前先调用ExportMethod将其导出,并设置其回调函数 135 mock.Struct(&Struct1{}).ExportMethod("call").Apply(func(_ *Struct1, i int) int { 136 return i * 2 137 }) 138 139 // mock 结构体Struct1的未导出方法call, mock前先调用ExportMethod将其导出为函数类型,后续支持设置When, Return等 140 // As调用之后,请使用Return或When API的方式来指定mock返回。 141 mock.Struct(&Struct1{}).ExportMethod("call").As(func(_ *Struct1, i int) int { 142 // 随机返回值即可; 因后面已经使用了Return,此函数不会真正被调用, 主要用于指定未导出函数的参数签名 143 return i * 2 144 }).Return(1) 145 ``` 146 147 ### 2. 接口Mock 148 接口定义举例: 149 ```golang 150 // I 接口测试 151 type I interface { 152 Call(int) int 153 Call1(string) string 154 call2(int32) int32 155 } 156 ``` 157 158 被测接口实例代码: 159 ```golang 160 // TestTarget 被测对象 161 type TestTarget struct { 162 // field 被测属性(接口类型) 163 field I 164 } 165 166 func NewTestTarget(i I) *TestTarget { 167 return &TestTarget{ 168 field:i, 169 } 170 } 171 172 func (t *TestTarget) Call(num int) int { 173 return field.Call(num) 174 } 175 176 func (t *TestTarget) Call1(str string) string { 177 return field.Call1(str) 178 } 179 ``` 180 181 接口属性/变量Mock示例: 182 ```golang 183 mock := mocker.Create() 184 185 // 初始化接口变量 186 i := (I)(nil) 187 188 // 将Mock应用到接口变量 189 // 1. interface mock只对mock.Interface(&目标接口变量) 的目标接口变量生效, 因此需要将被测逻辑结构中的I类型属性或变量替换为i,mock才可生效 190 // 2. 一般建议使用struct mock即可。 191 // 3. Apply调用的第一个参数必须为*mocker.IContext, 作用是指定接口实现的接收体; 后续的参数原样照抄。 192 mock.Interface(&i).Method("Call").Apply(func(ctx *mocker.IContext, i int) int { 193 return 100 194 }) 195 196 // =============================================================================== 197 // !!! 如果是mock interface的话,需要将interface i变量赋值替换【被测对象】的【属性】,才能生效 198 // 也就是说,不对该接口的所有实现类实例生效。 199 t := NewTestTarget(i) 200 201 // 断言mock生效 202 s.Equal(100, t.Call(1), "interface mock check") 203 204 mock.Interface(&i).Method("Call1").As(func(ctx *mocker.IContext, i string) string { 205 // 随机返回值即可; 因后面已经使用了Return,此函数不会真正被调用, 主要用于指定未导出函数的参数签名 206 return "" 207 }).When("").Return("ok") 208 s.Equal("ok", t.Call1(""), "interface mock check") 209 210 // Mock重置, 接口变量将恢复原来的值 211 mock.Reset() 212 s.Equal(nil, i, "interface mock reset check") 213 ``` 214 215 ### 3. 高阶用法 216 #### 3.1. 外部package的未导出函数mock(一般不建议对不同包下的未导出函数进行mock) 217 ```golang 218 // 针对其它包的mock示例 219 // 创建指定包的mocker,设置引用路径 220 mock := mocker.Create() 221 222 // mock函数foo1并设置其回调函数 223 mock.Pkg("github.com/tencent/goom_test").ExportFunc("foo1").Apply(func(i int) int { 224 return i * 3 225 }) 226 227 // mock函数foo1并设置其返回值 228 mock.ExportFunc("foo1").As(func(i int) int { 229 // 随机返回值即可; 因后面已经使用了Return,此函数不会真正被调用, 主要用于指定未导出函数的参数签名 230 return 0 231 }).Return(1) 232 ``` 233 234 #### 3.2. 外部package的未导出结构体的mock(一般不建议对不同包下的未导出结构体进行mock) 235 ```golang 236 // 针对其它包的mock示例 237 package https://github.com/tencent/goom/a 238 239 // struct2 要mock的目标结构体 240 type struct2 struct { 241 field1 <type> 242 // ... 243 } 244 245 ``` 246 247 Mock代码示例: 248 ```golang 249 package https://github.com/tencent/goom/b 250 251 // fake fake一个结构体, 用于作为回调函数的Receiver 252 type fake struct { 253 // fake结构体要和原未导出结构体的内存结构对齐 254 // 即: 字段个数、顺序、类型必须一致; 比如: field1 <type> 如果有 255 // 此结构体无需定义任何方法 256 field1 <type> 257 // ... 258 } 259 260 // 创建指定包的mocker,设置引用路径 261 mock := mocker.Create() 262 263 // mock其它包的未导出结构体struct2的未导出方法call,并设置其回调函数 264 // 如果参数是未导出的,那么需要在当前包fake一个同等结构的struct(只需要fake结构体,方法不需要fake),fake结构体要和原未导出结构体struct2的内存结构对齐 265 // 注意: 如果方法是指针方法,那么需要给struct加上*,比如:ExportStruct("*struct2") 266 mock.Pkg("https://github.com/tencent/goom/a").ExportStruct("struct2").Method("call").Apply(func(_ *fake, i int) int { 267 return 1 268 }) 269 s.Equal(1, struct2Wrapper.call(0), "unexported struct mock check") 270 271 // mock其它包的未导出结构体struct2的未导出方法call,并设置其返回值 272 mock.ExportStruct("struct2").Method("call").As(func(_ *fake, i int) int { 273 // 随机返回值即可; 因后面已经使用了Return,此函数不会真正被调用, 主要用于指定接口方法的参数签名 274 return 0 275 }).Return(1) // 指定返回值 276 s.Equal(1, struct2Wrapper.call(0), "unexported struct mock check") 277 ``` 278 279 ### 4. 追加多个返回值序列 280 ```golang 281 mock := mocker.Create() 282 283 // 设置函数foo当传入参数为1时,第一次返回3,第二次返回2 284 when := mock.Func(foo).When(1).Return(0) 285 for i := 1;i <= 100;i++ { 286 when.AndReturn(i) 287 } 288 s.Equal(0, foo(1), "andReturn result check") 289 s.Equal(1, foo(1), "andReturn result check") 290 s.Equal(2, foo(1), "andReturn result check") 291 ... 292 ``` 293 294 ### 5. 在回调函数中调用原函数 295 ```golang 296 mock := mocker.Create() 297 298 // 定义原函数,用于占位,实际不会执行该函数体 299 // 需要和原函数的参数列表保持一致 300 // 定义原函数,用于占位,实际不会执行该函数体 301 var origin = func(i int) int { 302 // 用于占位, 实际不会执行该函数体; 因底层trampoline技术的占位要求, 必须编写方法体 303 fmt.Println("only for placeholder, will not call") 304 // return 指定随机返回值即可 305 return 0 306 } 307 308 mock.Func(foo1).Origin(&origin).Apply(func(i int) int { 309 // 调用原函数 310 originResult := origin(i) 311 312 // 加入延时逻辑等 313 time.Sleep(time.Seconds) 314 315 return originResult + 100 316 }) 317 // foo1(1) 等待1秒之后返回:101 318 s.Equal(101, foo1(1), "call origin result check") 319 ``` 320 321 ## 问题答疑 322 [问题答疑记录wiki地址](https://github.com/tencent/goom) 323 常见问题: 324 1. 如果是M1-MAC(arm CPU)机型, 可以尝试以下两种方案 325 326 a. 尝试使用权限修复工具,在项目根目录执行以下指令: 327 ```shell 328 MOCKER_DIR=$(go list -m -f '{{.Dir}}' github.com/tencent/goom) 329 ${MOCKER_DIR}/tool/permission_denied.sh -i 330 ``` 331 332 b: 如果a方案没有效果,则尝试切换成amd的go编译器,在环境变量中添加: 333 ```shell 334 GOARCH=amd64 335 ``` 336 337 2. 如果遇到mock未生效的问题,可以打开debug日志进行自助排查 338 ```go 339 // TestUnitTestSuite 测试入口 340 func TestUnitTestSuite(t *testing.T) { 341 // 开启debug模式, 在控制台可以 342 // 1.查看apply和reset的状态日志 343 // 2.查看mock调用日志 344 mocker.OpenDebug() 345 suite.Run(t, new(mockerTestSuite)) 346 } 347 ``` 348 349 ## Contributor 350 @yongfuchen、@adrewchen、@ivyyi、@miliao 351