51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 1086|回复: 0
打印 上一主题 下一主题

[原创] 带你走进Golang 单元测试(下)

[复制链接]
  • TA的每日心情
    无聊
    昨天 11:40
  • 签到天数: 943 天

    连续签到: 2 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2022-11-2 11:25:36 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
    使用 gomock 打桩
      最后剩下 getPersonDetailRedis 函数,我们先来看一下这个函数的逻辑。
    1. <font size="3"> // 通过 redis 拉取对应用户的资料信息

    2.   func getPersonDetailRedis(username string) (*PersonDetail, error) {

    3.    result := &PersonDetail{}

    4.    client, err := redis.Dial("tcp", ":6379")

    5.    defer client.Close()

    6.    data, err := redis.Bytes(client.Do("GET", username))

    7.    if err != nil {

    8.     return nil, err

    9.    }

    10.    err = json.Unmarshal(data, result)

    11.    if err != nil {

    12.     return nil, err

    13.    }

    14.    return result, nil

    15.   }</font>
    复制代码
    getPersonDetailRedis 函数的核心在于生成了 client 调用了它的 Do 方法,通过分析得知 client 实际上是一个符合 Conn 接口的结构体。如果我们使用 gomonkey 来进行打桩,需要先声明一个结构体并实现 Client 接口拥有的方法,之后才能使用 gomonkey 给函数打桩。
    1. <font size="3">// redis 包中关于 Conn 的定义

    2.   // Conn represents a connection to a Redis server.

    3.   type Conn interface {

    4.    // Close closes the connection.

    5.    Close() error

    6.    // Err returns a non-nil value when the connection is not usable.

    7.    Err() error

    8.    // Do sends a command to the server and returns the received reply.

    9.    Do(commandName string, args ...interface{}) (reply interface{}, err error)

    10.    // Send writes the command to the client's output buffer.

    11.    Send(commandName string, args ...interface{}) error

    12.    // Flush flushes the output buffer to the Redis server.

    13.    Flush() error

    14.    // Receive receives a single reply from the Redis server

    15.    Receive() (reply interface{}, err error)

    16.   }

    17.   // 实现接口

    18.   type Client struct {}

    19.   func (c *Client) Close() error {

    20.     return nil

    21.   }

    22.   func (c *Client) Err() error {

    23.     return nil

    24.   }

    25.   func (c *Client) Do(commandName string, args ...interface{}) (interface{}, error) {

    26.     return nil, nil

    27.   }

    28.   func (c *Client) Send(commandName string, args ...interface{}) error {

    29.     return nil

    30.   }

    31.   func (c *Client) Flush() error {

    32.     return nil

    33.   }

    34.   func (c *Client) Receive() (interface{}, error) {

    35.     return nil, nil

    36.   }

    37.   // 实现接口

    38.   type Client struct {}

    39.   func (c *Client) Close() error {

    40.    return nil

    41.   }

    42.   func (c *Client) Err() error {

    43.    return nil

    44.   }

    45.   func (c *Client) Do(commandName string, args ...interface{}) (interface{}, error) {

    46.    return nil, nil

    47.   }

    48.   func (c *Client) Send(commandName string, args ...interface{}) error {

    49.    return nil

    50.   }

    51.   func (c *Client) Flush() error {

    52.    return nil

    53.   }

    54.   func (c *Client) Receive() (interface{}, error) {

    55.    return nil, nil

    56.   }

    57.   // 进行测试

    58.   func test() {

    59.    c := &Client{}

    60.    gomonkey.ApplyFunc(redis.Dial, func(_ string, _ string, _ ...redis.DialOption) (redis.Conn, error) {

    61.     return c, nil

    62.    })

    63.    gomonkey.ApplyMethod(reflect.TypeOf(c), "Do", func(commandName string, args ...interface{}) (interface{}, error) {

    64.     var result interface{}

    65.     return result, nil

    66.    })

    67.   }</font>
    复制代码


    可见,如果接口实现的方法更多,那么打桩需要手写的代码会更多。因此这里需要一种能自动根据原接口的定义生成接口的 mock 代码以及更方便的接口 mock 方式。于是这里我们使用 gomock 来解决这个问题。
      本地安装 gomock


    1. <font size="3"># 打开终端后依次执行

    2.   go get -u github.com/golang/mock/gomock

    3.   go install github.com/golang/mock/mockgen

    4.   # 备注说明,很重要!!!

    5.   # 安装完成之后,执行 mockgen 看命令是否生效 # 如果显示命令无效,请找到本机的 GOPATH 安装目录下的 bin 文件夹是否有 mockgen 二进制文件

    6.   # GOPATH 可以执行 go env 命令找到

    7.   # 如果命令无效但是 GOPATH 路径下的 bin 文件夹中存在 mockgen,请将 GOPATH 下 bin 文件夹的绝对路径添加到全局 PATH 中</font>
    复制代码


    生成 gomock 桩代码
      安装完毕后,找到要进行打桩的接口,这里是 http://github.com/gomodule/redigo/redis 包里面的 Conn 接口。
      在当前代码目录下执行以下指令,这里我们只对某个特定的接口生成 mock 代码。


    1. <font size="3"> mockgen -destination=mock_redis.go -package=unit github.com/gomodule/redigo/redis Conn

    2.   # 更多指令参考:https://github.com/golang/mock#flags</font>
    复制代码


    完善 gomock 相关逻辑

    1. <font size="3">func Test_getPersonDetailRedis(t *testing.T) {

    2.    tests := []struct {

    3.     name    string

    4.     want    *PersonDetail

    5.     wantErr bool

    6.    }{

    7.     {name: "redis.Do err", want: nil, wantErr: true},

    8.     {name: "json.Unmarshal err", want: nil, wantErr: true},

    9.     {name: "success", want: &PersonDetail{

    10.      Username: "steven",

    11.      Email:    "1234567@qq.com",

    12.     }, wantErr: false},

    13.    }

    14.    ctrl := gomock.NewController(t)

    15.    defer ctrl.Finish()

    16.    // 1. 生成符合 redis.Conn 接口的 mockConn

    17.    mockConn := NewMockConn(ctrl)

    18.    // 2. 给接口打桩序列

    19.    gomock.InOrder(

    20.     mockConn.EXPECT().Do("GET", gomock.Any()).Return("", errors.New("redis.Do err")),

    21.     mockConn.EXPECT().Close().Return(nil),

    22.     mockConn.EXPECT().Do("GET", gomock.Any()).Return("123", nil),

    23.     mockConn.EXPECT().Close().Return(nil),

    24.     mockConn.EXPECT().Do("GET", gomock.Any()).Return([]byte(`{"username": "steven", "email": "1234567@qq.com"}`), nil),

    25.     mockConn.EXPECT().Close().Return(nil),

    26.    )

    27.    // 3. 给 redis.Dail 函数打桩

    28.    outputs := []gomonkey.OutputCell{

    29.     {

    30.      Values: gomonkey.Params{mockConn, nil},

    31.      Times:  3, // 3 个用例

    32.     },

    33.    }

    34.    patches := gomonkey.ApplyFuncSeq(redis.Dial, outputs)

    35.    // 执行完毕之后释放桩序列

    36.    defer patches.Reset()

    37.    // 4. 断言

    38.    for _, tt := range tests {

    39.     actual, err := getPersonDetailRedis(tt.name)

    40.     // 注意,equal 函数能够对结构体进行 deap diff

    41.     assert.Equal(t, tt.want, actual)

    42.     assert.Equal(t, tt.wantErr, err != nil)

    43.    }

    44.   }</font>
    复制代码


    从上面可以看到,给 getPersonDetailRedis 函数做单元测试主要做了四件事情:
      ·生成符合 redis.Conn 接口的 mockConn
      · 给接口打桩序列
      · 给函数 redis.Dial 打桩
      · 断言
      这里面同时使用了 gomock、gomonkey 和 testify 三个包作为压测工具,日常使用中,由于复杂的调用逻辑带来繁杂的单测,也无外乎使用这三个包协同完成。
      查看单测报告
      单元测试编写完毕之后,我们可以调用相关的指令来查看覆盖范围,帮助我们查看单元测试是否已经完全覆盖逻辑代码,以便我们及时调整单测逻辑和用例。
      使用 go test 指令
      默认情况下,我们在当前代码目录下执行 go test 指令,会自动的执行当前目录下面带 _test.go 后缀的文件进行测试。如若想展示具体的测试函数以及覆盖率,可以添加 -v 和 -cover 参数,如下所示:


    1. <font size="3">  go_unit_test [master]    go test -v -cover

    2.   === RUN   TestGetPersonDetail

    3.   --- PASS: TestGetPersonDetail (0.00s)

    4.   === RUN   Test_checkEmail

    5.   --- PASS: Test_checkEmail (0.00s)

    6.   === RUN   Test_checkUsername

    7.   --- PASS: Test_checkUsername (0.00s)

    8.   === RUN   Test_getPersonDetailRedis

    9.   --- PASS: Test_getPersonDetailRedis (0.00s)

    10.   PASS

    11.   coverage: 60.8% of statements

    12.   ok      unit    0.131s</font>
    复制代码


    如果想指定测试某一个函数,可以在指令后面添加 -run ${test文件内函数名} 来指定执行。

    1. <font size="3">   go_unit_test [master]    go test -cover -v  -run Test_getPersonDetailRedis

    2.   === RUN   Test_getPersonDetailRedis

    3.   --- PASS: Test_getPersonDetailRedis (0.00s)

    4.   PASS

    5.   coverage: 41.9% of statements

    6.   ok      unit    0.369s</font>
    复制代码


    在执行 go test 命令时,需要加上 -gcflags=all=-l 防止编译器内联优化导致单测出现问题,这跟打桩代码存在密切的关系,后面我们会详细的介绍这一点。
      因此,一个完整的单测指令可以是 go test -v -cover -gcflags=all=-l -coverprofile=coverage.out
      生成覆盖报告
      最后,我们可以执行 go tool cover -html=coverage.out ,查看代码的覆盖情况,使用前请先安装好 go tool 工具。





    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-5-8 07:11 , Processed in 0.063567 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表