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