【go的测试】单测之gomock包与gomonkey包
目录
使用gomock包
1. 安装mockgen
2. 定义接口
3. 生成mock文件
4. 在单测中使用mock的函数
5. gomock 包的使用问题
使用gomonkey包
1. mock 一个包函数
2. mock 一个公有成员函数
3. mock 一个私有成员函数
使用gomock包
1. 安装mockgen
go get -u github.com/golang/mock/gomock
go get -u github.com/golang/mock/mockgen
2. 定义接口
比如我们想要 mock 一个 infra 包中的名为 GetFromRedis 的函数:
// pkg/infra/infra.go
package infrafunc GetFromRedis(cli *redis.Client, key string) (string, error) {// 为什么这里要打印一下呢,因为一行会触发内联优化,影响单测结果...fmt.Println("cannot reach here if is ut")return cli.Get(context.Background(), key).Result()
}
那需要定义一个 interface 将此GetFromRedis 的函数封装起来:
type Redis interface {GetFromRedis(cli *redis.Client, key string) (string, error)
}
这个 interface 必须定义,否则 gomock 无法生成 mock 方法。
3. 生成mock文件
在命令行执行:
mockgen -source=pkg/infra/infra.go -destination=pkg/infra/mock/mock_infra.go -package=mock
其中 source 为待 mock 的文件,destination 为 mock 文件生成的位置。
mockgen 工具将为你生成 pkg/infra/mock/mock_infra.go 文件。
4. 在单测中使用mock的函数
mock 就可以这样写:
// pkg/infra/infra_test.gofunc Test_gomock(t *testing.T) {// 创建gomock控制器ctrl := gomock.NewController(t)defer ctrl.Finish()var redisCli *redis.Client// 创建模拟对象m := mock.NewMockRedis(ctrl)// 定义预期行为m.EXPECT().GetFromRedis(redisCli, "test_key"). // 模拟入参Return("test value", nil) // 模拟返回值// 调用被测方法,获取实际结果resVal, err := m.GetFromRedis(redisCli, "test_key")assert.NoError(t, err)assert.Equal(t, "test value", resVal)
}
5. gomock 包的使用问题
- 用起来很麻烦,要定义 interface,还要执行 mockgen 命令
- 无法 mock 私有成员的函数
使用gomonkey包
当你用了 gomonkey 之后!就再也不会想用 gomock 了!(gomonkey打钱)
1. mock 一个包函数
还是上述例子,比如我们想要 mock 一个 infra 包中的名为 GetFromRedis 的函数:
// pkg/infra/infra.go
package infrafunc GetFromRedis(cli *redis.Client, key string) (string, error) {fmt.Println("cannot reach here if is ut")return cli.Get(context.Background(), key).Result()
}
无需前置操作,直接在单测中写 mock 代码:
// pkg/infra/infra_test.gofunc Test_gomonkey_function(t *testing.T) {// 创建gomonkey对象, 模拟GetFromRedis预期行为patch := gomonkey.ApplyFunc(GetFromRedis, func(_ *redis.Client, _ string) (string, error) {return "test value", nil})defer patch.Reset()// 调用被测方法,获取实际结果resVal, err := GetFromRedis(&redis.Client{}, "test_key")assert.NoError(t, err)assert.Equal(t, "test value", resVal)
}
ApplyFunc 的第一个参数是被测函数,第二个参数是预期的行为函数(注意入参出参定义要与被测函数完全一致)
当然你还可以用 patch 去 mock 更多函数:
patch.ApplyFunc(...)
2. mock 一个公有成员函数
比如我们有如下公有成员函数 PublicRedisHandler.GetFromRedis:
// pkg/infra/infra.go
package infratype PublicRedisHandler struct {cli *redis.Client
}func NewPublicRedisHandler(cli *redis.Client) *PublicRedisHandler {return &PublicRedisHandler{cli: cli,}
}func (r *PublicRedisHandler) GetFromRedis(key string) (string, error) {fmt.Println("cannot reach here if is ut")return r.cli.Get(context.Background(), key).Result()
}
在单测中写法为:
// pkg/infra/infra_test.gofunc Test_gomonkey_public_member(t *testing.T) {// mock 公共成员方法patch := gomonkey.ApplyMethod(reflect.TypeOf(&PublicRedisHandler{}), "GetFromRedis", func(_ *PublicRedisHandler, key string) (string, error) {return "test value", nil})defer patch.Reset()// 调用被测方法,获取实际结果r := NewPublicRedisHandler(&redis.Client{})resVal, err := r.GetFromRedis("test_key")assert.NoError(t, err)assert.Equal(t, "test value", resVal)
}
其中 ApplyMethod 的第一个参数是成员的类型,第二个参数是函数名称,第三个参数是预期的行为函数(注意入参的第一个参数必须是成员对象)
3. mock 一个私有成员函数
那如果我们想要 mock 的函数是一个私有成员下的函数呢:
// pkg/infra/infra.go
package infratype privateRedisHandler struct {cli *redis.Client
}func newewPrivateRedisHandler(cli *redis.Client) *privateRedisHandler {return &privateRedisHandler{cli: cli,}
}func (r *privateRedisHandler) GetFromRedis(key string) (string, error) {// 为什么这里要打印一下呢,因为一行会触发内联优化,影响单测结果...fmt.Println("cannot reach here if is ut")return r.cli.Get(context.Background(), key).Result()
}
那么写法为:
// pkg/infra/infra_test.gofunc Test_gomonkey_private_member(t *testing.T) {// mock 私有成员方法patch := gomonkey.ApplyPrivateMethod(reflect.TypeOf(&privateRedisHandler{}), "GetFromRedis", func(_ *privateRedisHandler, key string) (string, error) {return "test value", nil})defer patch.Reset()// 调用被测方法,获取实际结果r := newPrivateRedisHandler(&redis.Client{})resVal, err := r.GetFromRedis("test_key")assert.NoError(t, err)assert.Equal(t, "test value", resVal)
}
其中 ApplyPrivateMethod 的第一个参数是成员的类型,第二个参数是函数名称,第三个参数是预期的行为函数(注意入参的第一个参数必须是成员对象)