开发工具 从 Go 语言的依赖库讲起 1:Ginkgo、testify 和 GoMock

kevin · 2020年02月14日 · 最后由 astaxie 回复于 2020年02月14日 · 1565 次阅读
本帖已被设为精华帖!

对开发而言,测试的重要性相信对每个开发者而言是老生常谈的事情。虽然我们很有可能在开发过程中由于各种原因会希望后续补全,然而事实上我更建议采用 “Tests that fail then pass” 原则去处理在实际开发过程中遇到的问题。

在我们开发过程的初期阶段,开发质量的保持更多依赖开发人员自身素质保持。但是对一个团队而言,未必能够一直保持人员的高素质开发。在这个过程中,人员的变动,新老编码习惯的冲突,人员能力的残次不齐都有可能导致代码的腐化。在测试过程中,我们选择引入测试保障代码的质量

Go 本身提供了基础的测试功能,但是这个功能在实际使用过程中仍有使用起来功能较弱的问题。比如我们在使用过程中,需要使用额外的库让测试代码更佳高效。在实际实践过程中,我推荐使用GinkgotestifyGoMock工具。

GoMock

GoMock 工具是 Golang 官方提供的针对接口的代码生成测试工具。在实际的单元测试过程中,通常会选择 Mock 掉数据库(DB/KV)、外部服务调用操作部分,将这部分功能留在集成测试中完成。

比如我们将数据操作类型抽象成接口CreatorUpdaterDeleter等,借助接口的组合功能,针对我们需要的功能进行组合开发。在测试过程中,我们可以借助 GoMock 工具生成对应的测试辅助代码。

以对最简单的io.ReadeCloser使用代码为例:

package tdd

import "io"

func Read(r io.ReadCloser, buf []byte) (n int, err error) {
    n, err = io.ReadFull(r, buf)
    return
}

生成对应的 mock 方法,这里为了方便,我们使用-package参数定义包名,为了区分生成文件,添加了_ten_test.go后缀。

# 指定生成io.ReadCloser的mock方法
# 如果有专门的文件定义对应接口定义,则可以通过-source方法指定一次性提取所有接口
mockgen -package tdd io ReadCloser > reader_gen_test.go

接下来就是使用这个方法进行操作了,我们可以在reader_test.go文件中进行:

package tdd

import (
    "io"
    "reflect"
    "testing"

    "github.com/golang/mock/gomock"
)

func TestRead(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    r := NewMockReadCloser(ctrl)
    r.EXPECT().
        Read(gomock.AssignableToTypeOf([]byte{})).
        SetArg(0, []byte{0x0, 0x1, 0x2, 0x3, 0x4}). // 设置参数值
        Return(5, io.EOF).                          // 设置返回值
        AnyTimes()                                  // 执行次数

    buf := make([]byte, 5)
    Read(r, buf)
    want := []byte{0x0, 0x1, 0x2, 0x3, 0x4}
    if !reflect.DeepEqual(want, buf) {
        t.Errorf("Read() failed. want=%v, got=%v.", want, buf)
    }
}

testify

我们在上面的例子中,会发现使用 reflect.DeepEqual 方式对比,然后调用 t.Errorf 方式输出错误信息。但是这里面其实相对来说要麻烦一点,另外一个则是对数据而言,如果内容较多,我们没办法一一对比可能出现的内容,这种情况下testify工具则可以提供一种更便捷的方式帮助我们进行测试的管理。

为了方便对比这个测试内容,我们把上面DeepEqual的判断条件取反,获取的错误的内容对比验证一下:

# DeepEqual
=== RUN   TestRead
--- FAIL: TestRead (0.00s)
    /Users/kevin/Desktop/tdd/reader_test.go:26: Read() failed. want=[0 1 2 3 4], got=[0 1 2 3 4].
FAIL

现在,我们将测试文件替换为testify方式进行:

package tdd

import (
    "io"
    "testing"

    "github.com/golang/mock/gomock"
    "github.com/stretchr/testify/assert"
)

func TestRead(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    r := NewMockReadCloser(ctrl)
    r.EXPECT().
        Read(gomock.AssignableToTypeOf([]byte{})).
        SetArg(0, []byte{0x0, 0x1, 0x2, 0x3, 0x4}). // 设置参数值
        Return(5, io.EOF).                          // 设置返回值
        AnyTimes()                                  // 执行次数

    buf := make([]byte, 5)
    Read(r, buf)
    want := []byte{0x0, 0x1, 0x2, 0x3}
    if !assert.Equal(t, want, buf, "Read failed") {
        return
    }
}

获取测试结果:

=== RUN   TestRead
--- FAIL: TestRead (0.00s)
    /Users/kevin/Desktop/tdd/reader_test.go:25: 
            Error Trace:    reader_test.go:25
            Error:          Not equal: 
                            expected: []byte{0x0, 0x1, 0x2, 0x3}
                            actual  : []byte{0x0, 0x1, 0x2, 0x3, 0x4}

                            Diff:
                            --- Expected
                            +++ Actual
                            @@ -1,3 +1,3 @@
                            -([]uint8) (len=4) {
                            - 00000000  00 01 02 03                                       |....|
                            +([]uint8) (len=5) {
                            + 00000000  00 01 02 03 04                                    |.....|
                             }
            Test:           TestRead
            Messages:       Read failed
FAIL
coverage: 100.0% of statements

另外,在testify工具中,还提供了assert.JSONEq等等非常有用的函数,可以自行研究一下。同时,testify工具还提供了Testsuite功能,用于方便的设置 Setup 和 Teardown 函数。

你会发现testify工具还提供了mock功能,不过在实际过程中,不太建议使用该功能

Ginkgo

Ginkgo是针对 Go 程序进行 BDD 开发的工具,虽然它默认搭配使用gomega工具,不过我们还是建议你选择testify工具。你可以使用下面的方法快速接入testify

package foo_test

import (
    . "github.com/onsi/ginkgo"

    "github.com/stretchr/testify/assert"
)

var _ = Describe(func("foo") {
    It("should testify to its correctness", func(){
        assert.Equal(GinkgoT(), foo{}.Name(), "foo")
    })
})

Ginkgo工具提供了完善的文档介绍,你可以参考工具官方文档了解具体的使用。另外一个Ginkgo非常有用的是它可以方便接入已有的测试日志捕获程序,比如你是 JUnit 的用户,你可以选择将日志格式输出成 JUnit XML 格式:

package foo_test

import (
    . "github.com/onsi/ginkgo"
    . "github.com/onsi/gomega"

    "github.com/onsi/ginkgo/reporters"
    "testing"
)

func TestFoo(t *testing.T) {
    RegisterFailHandler(Fail)
    junitReporter := reporters.NewJUnitReporter("junit.xml")
    RunSpecsWithDefaultAndCustomReporters(t, "Foo Suite", []Reporter{junitReporter})
}

总结

文章总结了一些常见的涉及测试的工具,希望对你在实践过程中有所帮助。顺带,我还没忘记要完成这个系列。:D

查看原文

更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
astaxie 将本帖设为了精华贴 02月14日 20:48

很赞,写代码非常实用

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册