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)传入
测试示例
- 定义待测试内容
// 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)
}
- 执行 mock 类生成命令
在 a.go 所在的文件夹执行go generate
,可以看到生成了 mock 文件
- 在 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
- 通常 monkey patch 是用于 UT 的,但是正式环境也能使用,build 时注意用
- 目前在 OSX 10.10.2 和 Ubuntu 14.04 运行正常,预计在 unix-based x86 或 x86-64 都可以运行
- 需要系统支持内存即可修改又可执行
测试示例
- 定义待测试内容
// 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"
}
- 在 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”。