使用 gomock 打桩
最后剩下 getPersonDetailRedis 函数,我们先来看一下这个函数的逻辑。
// 通过 redis 拉取对应用户的资料信息
func getPersonDetailRedis(username string) (*PersonDetail, error) {
result := &PersonDetail{}
client, err := redis.Dial("tcp", ":6379")
defer client.Close()
data, err := redis.Bytes(client.Do("GET", username))
if err != nil {
return nil, err
}
err = json.Unmarshal(data, result)
if err != nil {
return nil, err
}
return result, nil
}
getPersonDetailRedis 函数的核心在于生成了 client 调用了它的 Do 方法,通过分析得知 client 实际上是一个符合 Conn 接口的结构体。如果我们使用 gomonkey 来进行打桩,需要先声明一个结构体并实现 Client 接口拥有的方法,之后才能使用 gomonkey 给函数打桩。
// redis 包中关于 Conn 的定义
// Conn represents a connection to a Redis server.
type Conn interface {
// Close closes the connection.
Close() error
// Err returns a non-nil value when the connection is not usable.
Err() error
// Do sends a command to the server and returns the received reply.
Do(commandName string, args ...interface{}) (reply interface{}, err error)
// Send writes the command to the client's output buffer.
Send(commandName string, args ...interface{}) error
// Flush flushes the output buffer to the Redis server.
Flush() error
// Receive receives a single reply from the Redis server
Receive() (reply interface{}, err error)
}
// 实现接口
type Client struct {}
func (c *Client) Close() error {
return nil
}
func (c *Client) Err() error {
return nil
}
func (c *Client) Do(commandName string, args ...interface{}) (interface{}, error) {
return nil, nil
}
func (c *Client) Send(commandName string, args ...interface{}) error {
return nil
}
func (c *Client) Flush() error {
return nil
}
func (c *Client) Receive() (interface{}, error) {
return nil, nil
}
// 实现接口
type Client struct {}
func (c *Client) Close() error {
return nil
}
func (c *Client) Err() error {
return nil
}
func (c *Client) Do(commandName string, args ...interface{}) (interface{}, error) {
return nil, nil
}
func (c *Client) Send(commandName string, args ...interface{}) error {
return nil
}
func (c *Client) Flush() error {
return nil
}
func (c *Client) Receive() (interface{}, error) {
return nil, nil
}
// 进行测试
func test() {
c := &Client{}
gomonkey.ApplyFunc(redis.Dial, func(_ string, _ string, _ ...redis.DialOption) (redis.Conn, error) {
return c, nil
})
gomonkey.ApplyMethod(reflect.TypeOf(c), "Do", func(commandName string, args ...interface{}) (interface{}, error) {
var result interface{}
return result, nil
})
}
可见,如果接口实现的方法更多,那么打桩需要手写的代码会更多。因此这里需要一种能自动根据原接口的定义生成接口的 mock 代码以及更方便的接口 mock 方式。于是这里我们使用 gomock 来解决这个问题。
本地安装 gomock
# 打开终端后依次执行
go get -u github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen
# 备注说明,很重要!!!
# 安装完成之后,执行 mockgen 看命令是否生效 # 如果显示命令无效,请找到本机的 GOPATH 安装目录下的 bin 文件夹是否有 mockgen 二进制文件
# GOPATH 可以执行 go env 命令找到
# 如果命令无效但是 GOPATH 路径下的 bin 文件夹中存在 mockgen,请将 GOPATH 下 bin 文件夹的绝对路径添加到全局 PATH 中
生成 gomock 桩代码
安装完毕后,找到要进行打桩的接口,这里是 http://github.com/gomodule/redigo/redis 包里面的 Conn 接口。
在当前代码目录下执行以下指令,这里我们只对某个特定的接口生成 mock 代码。
mockgen -destination=mock_redis.go -package=unit github.com/gomodule/redigo/redis Conn
# 更多指令参考:https://github.com/golang/mock#flags
完善 gomock 相关逻辑
func Test_getPersonDetailRedis(t *testing.T) {
tests := []struct {
name string
want *PersonDetail
wantErr bool
}{
{name: "redis.Do err", want: nil, wantErr: true},
{name: "json.Unmarshal err", want: nil, wantErr: true},
{name: "success", want: &PersonDetail{
Username: "steven",
Email: "1234567@qq.com",
}, wantErr: false},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 1. 生成符合 redis.Conn 接口的 mockConn
mockConn := NewMockConn(ctrl)
// 2. 给接口打桩序列
gomock.InOrder(
mockConn.EXPECT().Do("GET", gomock.Any()).Return("", errors.New("redis.Do err")),
mockConn.EXPECT().Close().Return(nil),
mockConn.EXPECT().Do("GET", gomock.Any()).Return("123", nil),
mockConn.EXPECT().Close().Return(nil),
mockConn.EXPECT().Do("GET", gomock.Any()).Return([]byte(`{"username": "steven", "email": "1234567@qq.com"}`), nil),
mockConn.EXPECT().Close().Return(nil),
)
// 3. 给 redis.Dail 函数打桩
outputs := []gomonkey.OutputCell{
{
Values: gomonkey.Params{mockConn, nil},
Times: 3, // 3 个用例
},
}
patches := gomonkey.ApplyFuncSeq(redis.Dial, outputs)
// 执行完毕之后释放桩序列
defer patches.Reset()
// 4. 断言
for _, tt := range tests {
actual, err := getPersonDetailRedis(tt.name)
// 注意,equal 函数能够对结构体进行 deap diff
assert.Equal(t, tt.want, actual)
assert.Equal(t, tt.wantErr, err != nil)
}
}
从上面可以看到,给 getPersonDetailRedis 函数做单元测试主要做了四件事情:
·生成符合 redis.Conn 接口的 mockConn
· 给接口打桩序列
· 给函数 redis.Dial 打桩
· 断言
这里面同时使用了 gomock、gomonkey 和 testify 三个包作为压测工具,日常使用中,由于复杂的调用逻辑带来繁杂的单测,也无外乎使用这三个包协同完成。
查看单测报告
单元测试编写完毕之后,我们可以调用相关的指令来查看覆盖范围,帮助我们查看单元测试是否已经完全覆盖逻辑代码,以便我们及时调整单测逻辑和用例。
使用 go test 指令
默认情况下,我们在当前代码目录下执行 go test 指令,会自动的执行当前目录下面带 _test.go 后缀的文件进行测试。如若想展示具体的测试函数以及覆盖率,可以添加 -v 和 -cover 参数,如下所示:
go_unit_test [master] go test -v -cover
=== RUN TestGetPersonDetail
--- PASS: TestGetPersonDetail (0.00s)
=== RUN Test_checkEmail
--- PASS: Test_checkEmail (0.00s)
=== RUN Test_checkUsername
--- PASS: Test_checkUsername (0.00s)
=== RUN Test_getPersonDetailRedis
--- PASS: Test_getPersonDetailRedis (0.00s)
PASS
coverage: 60.8% of statements
ok unit 0.131s
如果想指定测试某一个函数,可以在指令后面添加 -run ${test文件内函数名} 来指定执行。
go_unit_test [master] go test -cover -v -run Test_getPersonDetailRedis
=== RUN Test_getPersonDetailRedis
--- PASS: Test_getPersonDetailRedis (0.00s)
PASS
coverage: 41.9% of statements
ok unit 0.369s
在执行 go test 命令时,需要加上 -gcflags=all=-l 防止编译器内联优化导致单测出现问题,这跟打桩代码存在密切的关系,后面我们会详细的介绍这一点。
因此,一个完整的单测指令可以是 go test -v -cover -gcflags=all=-l -coverprofile=coverage.out
生成覆盖报告
最后,我们可以执行 go tool cover -html=coverage.out ,查看代码的覆盖情况,使用前请先安装好 go tool 工具。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理