IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> 使用 Mock 和 Interface 进行 Golang 单测 -> 正文阅读

[开发测试]使用 Mock 和 Interface 进行 Golang 单测

在工作中我经常会发现很多工程师的 Golang 单测是写的有问题的,只是单纯的调用代码做输出,并且会包含各种 IO 操作,导致单测无法到处运行。

本文将介绍 Golang 中如何正确的做单测。

什么是单元测试?单元测试的特点

单元测试是质量保证十分重要的一环,好的单元测试不仅能及时地发现问题,更能够方便地调试,提高生产效率,所以很多人认为写单元测试是需要额外的时间,会降低生产效率,是对单元测试最大的偏见和误解。

单测会将对应测试的模块隔离出来进行测试,所以我们要尽可能把所有相关的外部依赖都移除,只对相关的模块进行单测。

所以大家看到的在业务代码仓库中的一些在 client 模块中调用 HTTP 的单测其实是不规范的,因为 HTTP 是外部依赖,你的目标服务器如果有故障,那么你的单测就会失败。

func Test_xxx(t *testing.T) {
    DemoClient := &demo.DemoClient{url: "http://localhost:8080"}
    DemoClient.Init()
    resp := DemoCliten.DoHTTPReq()
    fmt.Println(resp)
}

上面这个例子中 DemoClient 在做 DoHTTPReq 方法的时候,会调用 http.Get 方法,这个方法是中包含了外部依赖也就是说他会去请求本地的服务器,如果有一个新的同事刚拿到你的代码,并且本地是没有这个服务器的话,那么你的单测就会失败。

并且上面这个例子中,对于 DoHTTPReq 这个函数只是简单的做了一个输出,没有对返回值做任何检查,如果内部逻辑修改并且返回值修改了,虽然你的测试还是能够 pass 但是其实你的单测是不起作用的。

从上面的例子中我们可以总结出两个单测特点:

  • 没有外部依赖,尽量无副作用,能够到处运行
  • 对输出进行检查

此外我还想提及的一点就是对于单测的编写难度其实是有排序的:

UI > Service > Utils

所以对于单测的编写,我们会优先考虑对 Utils 的单测, 因为 Utils 不会有太多的依赖。其次是对于 Service 的单测,因为 Service 的单测主要是对上游服务和数据库的依赖,只需要分离出依赖就可以进行逻辑的测试。

那该如何分离出依赖呢?我们走到下一节,我们将会介绍如何分离出依赖。

什么是 Mock?

对于 IO 的依赖,我们可以使用 Mock 来模拟数据,这样我们就可以不用担心数据源不稳定的问题了。

那么什么是 Mock 呢?我们又该如何 Mock 呢? 可以想想一个场景,就是你和你的同事正在进行合作的项目开发,你这边的进展比较快,已经快完成你的开发了,但是你的同事进展稍慢,并且你还依赖他的服务。你要怎么不 block 你的进展继续开发呢?

这里就可以使用到 Mock 了,就是你可以和你的同事先制定好需要交互的数据格式,在你的测试代码中,你可以编写一个可以产生对应数据格式的客户端,并且这些数据都是虚假的,然后你就可以继续编写你的代码,等到你的同事完成了他那部分的代码,你就只需要将 Mock Clients替换为真实的 Clients 就可以了。这就是 Mock 的作用。

同样的,我们也可以使用 Mock 来对需要测试模块所依赖的数据进行模拟。下面就是一个例子:

package myapp_test
// TestYoClient provides mockable implementation of yo.Client.
type TestYoClient struct {
    SendFunc func(string) error
}
func (c *TestYoClient) Send(recipient string) error {
    return c.SendFunc(recipient)
}
func TestMyApplication_SendYo(t *testing.T) {
    c := &TestYoClient{}
    a := &MyApplication{YoClient: c}
    // Mock our send function to capture the argument.
    var recipient string
    c.SendFunc = func(s string) error {
        recipient = s
        return nil
    }
    // Send the yo and verify the recipient.
    err := a.Yo("susy")
    ok(t, err)
    equals(t, "susy", recipient)
}

我们会有一个 MyApplication 同时他又依赖了一个发送上报的 YoClient。上面的代码中我们会将依赖的 YoClient 替换为 TestYoClient,这样代码在调用 MyApplication.Yo 时,其实执行的是 TestYoClient.Send,这样我们就可以自定义外部依赖的输入和输出了。

同样可以发现一个很有趣的地方,就是我们将 TestYoClientSendFunc 替换为 func(string) error,这样我们就可以更加灵活的控制输入和输出了。对于不同的测试,我们只需要更改 SendFunc 的值就可以了。这样每个测试我们都可以随意的控制输入和输出。

此时你会发现另外一个问题,就是如果想要将 TestYoClient 成功注入到 MyApplication 中,对应的成员变量就需要是 TestYoClient 这个具体类型或者是满足 Send() 方法的接口类型。但是如果我们使用具体类型的话,井没办法做到真实的 Client 与 Mock Client 的替换了。

所以我们可以使用接口类型来替换。

什么是 Interface?

在 Golang 中接口可能和你接触的其他语言的接口不同,在 Golang 中接口是一个函数的集合。并且 Golang 的接口是隐式的,不需要显式的定义。

我个人也非常同意这种设计,因为经过多次的实践,我发现事先定义好的抽象往往都是无法很准确的描述具体实现的行为的。所以需要事后做抽象, 与其写类型来满足 interface,不如写接口来满足使用要求。

Always [abstract] things when you actually need them, never when you just foresee that you need them.

我个人的推荐是对于几个相似的流程,我们可以先通过写几个结构体来组织代码,然后发现这些结构体有相似的行为之后,就可以抽象出一个接口来描述这些行为,这样才是最准确的。

同时对于 Golang 中的接口所包含的方法数目也需要加以限制,不能太多,1-3 个方法就可以了。其中的原因是这样的,如果你的接口中包含太多方法,你新增一个实现类型的代码就会很麻烦,而且这样的代码也不好维护。同样如果你的接口有很多种实现,并且方法有很多,那么你再多添加一个函数加入接口也会很困难,你需要在每个结构体中都实现这些方法。

回到主题,对于 YoClient 来说,最初如果我们不是采用 TDD 的方式,那么 MyApplication 的所依赖的一定是一个正式的具体类型,此时我们可以在测试代码中写一个 TestYoClient 类型的实例,提取出共有的函数抽出接口,然后再去将 MyApplication 中的 YoClient 替换为接口类型。

这样就可以达成我们的目的了。

package myapp
type MyApplication struct {
    YoClient interface {
        Send(string) error
    }
}
func (a *MyApplication) Yo(recipient string) error {
    return a.YoClient.Send(recipient)
}

其他的一些例子

此外我还提供了一个例子供参考,主要是从正式生产的代码中找到的,屏蔽了敏感信息的例子。

这个例子是对 etcd 这个外部依赖的 mock。

type ETCD interface {
    GetWithTimeout(key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error)
    Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan
}

type MockEtcdClient struct {
    GetWithTimeoutFunc func(key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error)
    WatchFunc          func(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan
}

func (m MockEtcdClient) GetWithTimeout(key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
    return m.GetWithTimeoutFunc(key, opts...)
}

func (m MockEtcdClient) Watch(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan {
    return m.WatchFunc(ctx, key, opts...)
}

一个单测的例子:

func Test_saveTestConf(t *testing.T) {
    etcd := store.MockEtcdClient{
        GetWithTimeoutFunc: func(key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) {
            return &clientv3.GetResponse{
                Kvs: []*mvccpb.KeyValue{
                    {
                        Key:   []byte("/xxxx/xxx/config"),
                        Value: []byte("{\"xxx\":\"xxx\"}"),
                    },
                },
            }, nil
        },
        WatchFunc: func(ctx context.Context, key string, opts ...clientv3.OpOption) clientv3.WatchChan {
            return nil
        },
    }

    configKey, err := saveTestConf(etcd ,"xxxx", "/xxxx/xxx/config")
    if err != nil {
        t.Error(err)
    }
    assert.Equal(t, "/xxxx/xxx/config", configKey)
}

other tips

关于测试我这边还有一些你可能不知道的小技巧

Golang 的 internal 与 external 测试

对于一个包的导出方法以及变量你可以在同一个包下建立 test 文件来测试,只需要将包名后缀改为 _test 即可。这样就可以做到 black box testing。好处是你可以以调用者的视角来描述你的测试,而不是以内部的视角来写你的测试。 也可以作为一个示例给使用者看一下如何使用。

// in example.go
package example

var start int

func Add(n int) int {
  start += n
  return start
}

// in example_test.go
package example_test

import (
    "testing"

    . "bitbucket.org/splice/blog/example"
)

func TestAdd(t *testing.T) {
  got := Add(1)
  if got != 1 {
    t.Errorf("got %d, want 1", got)
  }
}

另外你也可以对未导出的方法和变量进行测试,可以创建一个尾缀为 _internal_test 的文件来标识你是想要测试未导出的方法和变量的。

总结

  1. Golang 单测的特点:

  2. 没有外部依赖,尽量无副作用,能够到处运行

  3. 需要对输出进行检查

  4. 可以作为一个示例给使用者看一下如何使用

  5. Golang 可以使用接口来替换依赖

有趣的链接推荐

最后最后和大家分享一下参考的链接,以及一些最近在看的好文,因为看的比较零散,一并写给大家,希望大家能够收获。

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2022-01-12 00:19:40  更:2022-01-12 00:22:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/18 5:34:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码