Go UT

metting Go UT 方法记录。

go mock

go mock 是 Go 官方维护的 interface UT 工具,可以直接和 Go UT 结合。

基本原理

假设 A 是被测试类,Ber 是被 A 依赖的接口,B 是实现了 Ber 的类。 我们可以用 go mock 工具生成 B1 类,实现 Ber,然后在创建 A 时把 B1 对象传进去,这样就能在 A 调用 B1 时指定 B1 的各种动作,从而对 A 进行 UT。

  • 前提:
    • 要求以 interface 方式设计类
    • 被测试类的依赖对象,可以在构造时(构造函数、literal)传入

测试示例

  1. 定义待测试内容
// a.go
//go:generate mockgen -source=a.go -destination=mock/a.go -package=mock
// 在这里指定执行`go generate`时要生成的文件 mock 位置,通常放入 mock 子包内的同名文件中,以便引用

// **必须** 定义接口 Ber
type Ber interface {
  func GetName() string
}

// 定义 A
type A struct {
  b Ber
}
// 这里可以不定义构造函数,但是对于不向外导出的类,构造函数中要传入 Ber
func NewA(b Ber) *A {
  return &A{b: b}
}
// 被测试函数
func (p *A) Check() error {
  if p == nil {
    return errors.New("nil A")
  } else if p.b.GetName() == "always B" {
    return nil
  }
  return errors.New("invalid name: " + p.b.GetName())
}


// b.go
// 实现接口 Ber
type B struct {}
func (p *B) GetName() string {
  return "always B"
}

// main.go
func main() {
  a := NewA(&B{})
  err := a.Check()
  fmt.Println(err)
}
  1. 执行 mock 类生成命令

在 a.go 所在的文件夹执行go generate,可以看到生成了 mock 文件

  1. 在 UT test 中使用 mock 指定行为
func TestACheck(t *testing.T) {

  cases := []struct {
    caseName string
    berName string
    errorHappend bool
  } {
    {
      caseName: "normal",
      berName: "always B",
    },
    {
      caseName: "error happend",
      berName: "wrong name",
      errorHappend: true,
    },
  }

  for _, testCase := range cases {
      fmt.Println("testing ", testCase.caseName)

      // 初始化 mock 对象
      ctrl := gomock.NewController(t)
		  mockB := mock.NewMockBer(ctrl)

      // 设置期待入参及返回值
      mockB.Expect().GetName().Return(testCase.berName)

      a := NewA(mockB)
      err := a.Check()
      assert.Equal(t, testCase.errorHappend, err != nil)
  }
}

常用函数

  • xxx.Expect().Func(gomock.Any())... 传入参数不做限制
  • xxx.Expect().xxx(...).return(...).AnyTimes() 调用次数不受限制

monkey patch

monkey patch 是一个开源库,用于 UT 时篡改既有函数。虽然该库已经两年没有更新了,但依然可以稳定运行。

基本原理

Go 是有 Runtime 的,所以在运行时,可以利用 Runtime 修改函数地址。

  • 对于内联函数无法篡改,可以用go test -gcflags=-l避免自动内联优化
    • 通常 monkey patch 是用于 UT 的,但是正式环境也能使用,build 时注意用-gcflags=-l
  • 目前在 OSX 10.10.2 和 Ubuntu 14.04 运行正常,预计在 unix-based x86 或 x86-64 都可以运行
  • 需要系统支持内存即可修改又可执行

测试示例

  1. 定义待测试内容
// a.go
package a

type Namer interface {
  func GetName() string
}

type A struct {
  name string
}
func (p *A) GetName() {
  if p == nil {
    return "empty"
  }
  return p.name
}

var gA *A

func Init(name string) {
  gA = &A{name: name}
}

func GetInstance() Namer {
  return gA
}

// main.go
func main() {
  Init("name main")
  tmpA := a.GetInstance()
  fmt.Println(tmpA.GetName()) // "name main"
}
  1. 在 UT 中用 monkey patch 指定行为
// 在测试时,可以直接指定返回结果

type B struct {
  Name string
}
func (p B) GetName() string {
  return B.Name
}

func testA(t *testing.T) {
  patch := monkey.Patch(a.GetInstance, func() Namer {
    return B{Name: "testName"}
  })
  defer patch.Unpatch()

  tmpA := a.GetInstance()
  fmt.Println(tmpA.GetName()) // "testName"
}

常见问题

  • 由于 Go 默认开启 inline 编译优化,所以有时会发生下面问题:

    • 直接以test xxx运行失败,monkey patch 未生效
    • 加 log fmt.Printf("pos 1") 后运行成功,去掉 log 就失效
    • 以 debug 模式运行就生效
    • -gcflags=-l 就生效,但是有些时候还需要 inline 编译,不想由于 UT 影响原有代码
  • 问题原因: 比如上面a.GetInstance函数较简单,编译时会自动进行 inline 优化,生成的可执行程序缺少了函数调用过程,无法在 runtime 篡改。

    • 当添加 log 时,由于 log 包含了反射,优化无法提高性能,自动跳过了优化
    • debug 时,由于需要可执行程序和代码的一一对应关系,所以会自动关闭 inline 优化
  • 解决方案: 在 monkey patch 对应的函数上加上//go:noinline,就可以对这个函数避免优化。 通常需要 patch 的也是比较简单的单例函数,这样就可以尽量减少对原有代码的影响。

常用函数

  • patch := monkey.Patch(a.GetInstance, func() Namer { ... }) 篡改指定全局函数

  • patch := monkey.PatchInstanceMethod(reflect.TypeOf(B), "GetName", func(_ *B) string { return "" }) 篡改某个类的实例函数

  • 如果在篡改某函数后,想在篡改函数内部在执行原函数,可以先声明var patch *monkey.PatchGuard, 然后在内部执行guard.Unpatch();defer guard.Restore()来暂时关闭篡改动作

综合使用

go mock 和 monkey patch 可以综合使用,比如用 go mock 来 mock “Namer”,然后用 monkey patch 来替换 “a.GetInstance”。