编写测试友好的Golang代码

kevin · · 1388 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

目前我们有大量的应用采用了Golang程序进行构建,但是在执行研发流程里我们会发现一些来自于静态编译程序的不便:相对于我们之前使用的Python语言程序而言,我们无法在程序功能的单元测试里大量的使用Mock方式来进行高效测试。

而这些东西往往可以在开发人员编写单元测试用例时有效的节省时间和一些额外的环境准备成本。因此,这也给我们的程序的单元覆盖率带来了很多麻烦的地方:一些依赖于额外验证和表现的情况或者小几率出现的情况需要复杂的模拟步骤,对开发进度和效率带来了一些额外的影响。如何编写一个测试友好的Golang程序成为一个无法绕开的问题。

从动态语言到静态语言

动态语言有良好的运行时修改属性,在运行时的动态修改函数,可以进行有效的Mock。比如在Python(以3为例,内置了unittest.mock标准库)程序中:

with patch.object(ProductionClass, 'method', return_value=None) as mock_method:
    thing = ProductionClass()
    thing.method(1, 2, 3)

自然而然的,我们想到了这样的用法:

var imp = func() bool {
    return true
}

func TestFunc(t *testing.T) {
    defer func(org func() bool) {
        imp = org
    }(imp)

    img = func() bool {
        return false
    }
    // testing or something else...
}

这样实现Mock是完全可以的,但是实际上会带来一些额外的问题,比如说在MVC框架中,我们正常采用的方式一般是这样的:

import (
   "models"
    ...
)

func A(ctx Context) error {
    ...
    data := models.Data()
    ...
}

这种方式则是无法在运行中进行动态Mock的,除非将其转换为参数方式进行调用。

func TestFunc(t *testing.T) {
    Convey("test", t, func() {
        defer func(org func() string) {
            models.Data = org  // Error: cannot assign to models.Data
        }(models.Data)

        models.Data = func() string {
            return "mocked!"
        }

        ....
    })
}

转成

// var data = models.Data
// in A: data := data()

func TestFunc(t *testing.T) {
    Convey("test", t, func() {
        defer func(org func() string) {
            data = org
        }(data)

        data = func() string {
            return "mocked!"
        }

    })
}

这样写法略微会多处大量的临时函数指针变量,如果是使用这种方式则需要额外的变量值的对应关系,测试完成后变量值需要恢复成原有指针(如果需要测试正常功能)。

从变量到接口

除了上面介绍的方法以外,是不是还有看起来稍微优雅一点的测试方法呢?我们尝试将上面的函数形式换成下面的接口形式,将interface对应的变量作为全局变量。

// main.go
var fetcher DataFetcherInterface

type DataFetcherInterface interface {
    Data() string
}

type DataFetcher struct {
}

func (d DataFetcher) Data() string {
    return "hello world!"
}

func Func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s", fetcher.Data())
}

func main() {
    fetcher = DataFetcher{}
    http.HandleFunc("/", Func)
    http.ListenAndServe("127.0.0.1:12821", nil)
}

这样的话我们就可以在测试文件里面定义一个FakeDataFetcher,实现相关的功能:

// main_test.go
type FakeDataFetcher struct {
}

func (f FakeDataFetcher) Data() string {
    return "mocked!"
}

func TestFunc(t *testing.T) {
    Convey("test", t, func() {
        defer func(org DataFetcherInterface) {
            fetcher = org
        }(fetcher)

        fetcher = FakeDataFetcher{}

        req, _ := http.NewRequest("GET", "http://example.com/", nil)
        w := httptest.NewRecorder()
        Func(w, req)
        So(w.Body.String(), ShouldEqual, "mocked!")
    })
}

这样可以减少变量的生成个数,同时,也可以通过FakeDataFetcher{}传入不同的参数,实现不同的Faker测试。值得注意的是,在这个interface方法中需要特别注意变量共享的线程安全问题。

依赖注入

上面两种方法似乎思路类似,除了这些方案之外,还有没有其他的方案呢?最后介绍一下依赖注入的方式,这种方式也可以与上面提到的接口方式搭配使用。这种方式实现起来比较简单方便,也非常适合利用在一些面向过程场景中。

// main.go
type EchoInterface interface {
    Echo() string
}

type Echoer struct {
}

func (e Echoer) Echo() string {
    return "hello world!"
}

func Echo(e EchoInterface) string {
    return e.Echo()
}

func main() {
    provider := Echoer{}
    fmt.Println(Echo(provider))
}

测试文件:

// main_test.go
type FakeEchoer struct {
}

func (f FakeEchoer) Echo() string {
    return "mocked!"
}

func TestFunc(t *testing.T) {
    Convey("test", t, func() {
        provider := FakeEchoer{}
        So(Echo(provider), ShouldEqual, "mocked!")
    })
}

总结

上面的几种测试方法基本上是通过固定的原型将代码转为测试友好的Golang代码。这样可以通过Mock,减少来自于其他数据和前置条件的影响,尽可能的降低代码开发的附加成本。


有疑问加站长微信联系(非本文作者)

本文来自:ipfans's Blog

感谢作者:kevin

查看原文:编写测试友好的Golang代码

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1388 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传