Go:测试库(GoConvey,testify,GoStub,GoMonkey)对比及简介


一、测试框架 stretchr/testify

1.stretchr/testify/assert

assert库是这样的一个库,它有一系列函数来适应各种各样不同的场景需求,下面是一个简单的判断值是否符合预期的demo:

func TestFunc1(t *testing.T) {
    name := "Bob"
    age := 10
    assert.Equal(t, "Bob", name, "name should equal to Bob")
    assert.Equal(t, 10, age, "age should equal to 10")
}

命令行输入go test -v,对应的结果是PASS。我们尝试将第二个assert.Equal的10改成11,观察效果:
在这里插入图片描述
可以看到,这个时候报错会报错误的行数、错误的原因、测试函数及所带的Messages。而有些assert库的断言函数基本都是这样的原型:

func AssertFunc(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool

参数名含义
t TestingT 当前测试函数的t *testing.T
expected interface{} 预期的结果
actual interface{} 实际计算得到的结果
msgAndArgs …interface{} 形如fmt.Printf中的格式化输出字符串

用的assert库断言函数如下:
Equal/NotEqual 判断expected与actual相等/不相等
Nil/NotNil 判断object为nil/不为nil
Empty/NotEmpty 判断object为void/不为void
NoError/Error 判断err无值/有值
Zero/NotZero 判断i为0/不为0
True/False 判断value为true/false
Len 判断object长度是否为length
Contains/NotContains 判断s在/不在contains中
Subset/NotSubset 判断subset是/不是list的子集
FileExists 判断path所示的文件是否存在
DirExists 判断path所示的文件夹是否存在

2.stretchr/testify/require

这个库和assert库几乎是一致的,但是require遇到FAIL的例子会马上停下来,不再测试后面的测试用例。下面是一个使用assert库和require库分别测试错误用例的例子:

func TestFunc1(t *testing.T) {
    name := "Bob"
    age := 10
    kilo := 1
    assert.Equal(t, "Bob", name, "name should equal to Bob")
    assert.Equal(t, 10, age, "age should equal to 10")
    assert.Zero(t, kilo, "kilo should be zero")
}

执行测试,可以发现就算14行的断言并不返回true,15行的断言也会被继续执行:
在这里插入图片描述

我们把所有的assert都换成require,再做一下测试:
在这里插入图片描述
可以发现当第二个测试没通过后,第三个测试就不会被执行了(相当于执行了一次t.Errorf)。

二、测试框架GoConvey

GoConvey是一种可以组织测试用例间的逻辑关系的一种测试框架,通过使用GoConvey提供的嵌套功能,可以把测试用例分在不同的测试要求下,从而实现一种直观的、可以看到覆盖范围的测试用例组合。

1. 单个测试用例下使用GoConvey

我们先来探究单个测试用例下怎么使用GoConvey,一个demo如图:

// IsOdd 判断给定数字是否为奇数
func IsOdd(n int) bool {
    return n%2 != 0
}
func TestIsOdd(t *testing.T) {
    Convey("TestIsOdd should return true when n is odd number", t, func() {
        n := 2
        So(IsOdd(n), ShouldBeTrue)
    })
}

我们可以看到,Convey的第一个参数指的是该测试的名字,第二个参数是testing.T的指针,第三个参数是一个闭包,用于测试,其中So是断言函数,ShouldBeTrue代表着只有true的情况下这个测试用例才能通过。

执行go test -v,可以得到结果:
在这里插入图片描述

如果故意改为不过,报错就是这样的:
在这里插入图片描述
通过的样例会在测试说明后接一个.,而不过的样例则会借一个x,并且指出错误函数和错误原因。

2. 多个独立测试用例下使用GoConvey

如果我需要做多个测试,而这些测试用例并没有什么联系(这里的联系是指在执行测试之前是否需要执行一些公用的初始化操作)的时候,就可以调用多次Convey来进行测试,一个demo如下:

func TestIsOdd(t *testing.T) {
    Convey("TestIsOdd should return false when n is even number", t, func() {
        n := 2
        So(IsOdd(n), ShouldBeFalse)
    })Convey("TestIsOdd should return true when n is odd number", t, func() {
        n := 1
        So(IsOdd(n), ShouldBeTrue)
    })
}

测试出来的结果可以发现,每个样例都会被输出测试结果:
在这里插入图片描述

像之前的require库,一旦有一个用例错误就会停下来,那么GoConvey是否也会这样呢,我们来将demo改一下:

func TestIsOdd(t *testing.T) {
    Convey("TestIsOdd should return false when n is even number", t, func() {
        n := 1
        So(IsOdd(n), ShouldBeFalse)
    })Convey("TestIsOdd should return true when n is odd number", t, func() {
        n := 2
        So(IsOdd(n), ShouldBeTrue)
    })
}

我们将两个都置为不符合预期的测试用例,如果GoConvey遇到错误测试用例就停下来,那么第二个测试用例将不会被执行。我们执行一下,得到结果:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

可以发现就算前面的测试用例是错的,测试函数还是会继续执行,直到将所有的测试用例都执行完。

如果我们的函数,有一些功能还没实现,对于某些测试用例来说不可用,那该怎么办呢?有一种最直接的方法就是给这个测试用例所在的Convey打注释,但是这样会导致测试函数代码里面有一大段注释,非常的不美观。GoConvey提供了两种方法可以忽略测试用例,一种是SkipConvey,用于直接忽略整个Convey。一种是SkipSo,用于忽略某个断言。SkipSo更适用于Convey嵌套时,忽略某个大Convey下的小Convey对应的So。一个demo如下:

func TestIsOdd(t *testing.T) {
    Convey("TestIsOdd should return false when n is even number", t, func() {
        n := 2
        So(IsOdd(n), ShouldBeFalse)
    })SkipConvey("TestIsOdd should return true when n is odd number", t, func() {
        n := 1
        So(IsOdd(n), ShouldBeTrue)
    })
}

在这里插入图片描述
SkipConvey里面无论是So还是SkipSo都没关系,整个Convey都会被忽略掉。执行测试,结果如下:可以看到,被Skip的测试用例会标记一个感叹号,并且最后的语句会说one or more sections skipped。如果改成SkipSo也是一样的输出结果,此处不再赘述。

3. 嵌套用例下使用GoConvey

如果说我们需要分割多个测试用例到多个组别里面,每个组别有一个或多个测试用例,这个时候我们就需要用结构化的方式组织我们的测试用例。GoConvey支持嵌套调用Convey,使得测试显示信息的时候可以直观地看出测试用例的结构。这样说可能相当抽象,我们还是来测试IsOdd以获得一个嵌套测试的直观例子:

func TestIsOdd(t *testing.T) {
    Convey("Testing IsOdd in range [0,10]", t, func() {
        Convey("TestIsOdd should return false when n is 2", func() {
            n := 2
            So(IsOdd(n), ShouldBeFalse)
        })Convey("TestIsOdd should return true when n is 1", func() {
            n := 1
            So(IsOdd(n), ShouldBeTrue)
        })
    })
    Convey("Testing IsOdd in range (10,20]", t, func() {
        Convey("TestIsOdd should return false when n is 12", func() {
            n := 12
            So(IsOdd(n), ShouldBeFalse)
        })Convey("TestIsOdd should be return true when n is 17", func() {
            n := 17
            So(IsOdd(n), ShouldBeTrue)
        })
    })
}

我们在这里组织了两组测试,一组的名字叫Testing IsOdd in range [0,10],旗下有两个测试用例n=2和n=1。另一组的名字叫Testing IsOdd in range [10,20],旗下有两个测试用例n=12和n=17。

我们执行go test -v来看看具体的输出信息是怎样的:
在这里插入图片描述
可以看到,测试完第一组之后,会输出总共测试了几个测试用例,然后再输出第二组的结果。这样子分层输出的架构清晰明了,也方便我们在测试的时候观察错误出在哪一组下,从而可以在逻辑上更清晰地定位到业务逻辑出错的地方。

三、测试框架GoStub

1. 什么叫打桩?

打桩简单地来说就是把一些代码段进行替代,而GoStub中主要可以做到的就是对一个全局变量进行打桩、对一个函数打桩和对一个过程打桩。

2. 利用GoStub对全局变量进行打桩

假设我们有一个全局变量Num,它可能会在其他地方被更改,假设我们有一个函数叫JudgeNum,来在别的函数内被定时调用,判断它是否大于100,我们可以写出这个demo:

package func
var Num int
func JudgeNum() bool {
    return Num > 100
}

那我们可以对这个Num在测试阶段进行mock,从而达到我们测试判断的效果,使用GoStub对Num打桩,有测试函数:

func TestJudgeNum(t *testing.T) {
    // Mock variable Num to 150.
    stub1 := gostub.Stub(&Num,150)
    defer stub1.Reset()
    Convey("JudgeNum should return true when Num equals to 150", t, func() {
        So(JudgeNum(), ShouldBeTrue)
    })
}

其中的defer stub1.Reset()是用于在测试结束后回滚所有的桩代码的(当然你也可以在Convey完之后手动调用Reset以打第二个桩)。我们做两个独立的测试,分别是Num=150和Num=80,判断两个是否都能输出正确的结果,有测试函数:

func TestJudgeNum(t *testing.T) {
    // Mock variable Num to 150.
    stub := gostub.Stub(&Num, 150)
    Convey("JudgeNum should return true when Num equals to 150", t, func() {
        So(JudgeNum(), ShouldBeTrue)
    })
    stub.Reset()
    stub = gostub.Stub(&Num, 80)
    Convey("JudgeNum should return false when Num equals to 80", t, func() {
        So(JudgeNum(), ShouldBeFalse)
    })
    stub.Reset()
}

执行一下测试,我们可以看到:
在这里插入图片描述

由此我们可以得知,stub.Reset可以将桩代码进行回滚,从而结束打桩或者进行一个新的打桩过程。当然,在本代码中不调用第一个stub.Reset也是可以的,因为gostub.Stub会将旧的桩代码覆盖掉,但是最好还是显式地调用一下stub.Reset。

3. 利用GoStub对函数变量进行打桩

GoStub并不支持直接对函数入口进行mock,而是采用了一种迂回的方法:将函数赋值给函数变量,并作为一个适配层的全局变量存放着。业务逻辑代码想调用函数的时候,如果为了测试方便,则应该通过函数变量调用函数。这个有点抽象,我们还是来看一个demo:

package function
var JudgeNumFunc = JudgeNum
​
func JudgeNum() bool {
    return Num > 100
}

这里我们还是使用函数JudgeNum,但不同的是,我们使用了一个函数变量托住了这个函数。打桩的时候也是对这个函数变量进行打桩,因为函数是没有取地址一说的。那么我们要对函数变量打桩,自然打完桩之后要调用函数变量(而不是调用函数本身)!测试的demo如下:

func TestJudgeNum(t *testing.T) {
    stub := gostub.StubFunc(&JudgeNumFunc, true)
    defer stub.Reset()
    Convey("After mocking JudgeNum, JudgeNum should always return true", t, func() {
        So(JudgeNumFunc(), ShouldBeTrue)
    })
}

执行go test -v后,是PASS的。但如果把测试demo改成这样:

func TestJudgeNum(t *testing.T) {
    stub := gostub.StubFunc(&JudgeNumFunc, true)
    defer stub.Reset()
    Convey("After mocking JudgeNum, JudgeNum should always return true", t, func() {
        So(JudgeNum(), ShouldBeTrue)
    })
}

则该测试样例不会被通过,因为Num在没有被赋值的情况下,默认初始化为0。并且这样的测试也是不安全的,因为Num可能为0,也可能已经被别的函数修改为100以上了,这个测试是有可能通过的,但是JudgeNum的桩并没有打到,我们称这种测试通过是假的通过。

4. 利用GoStub对过程进行打桩

过程指的就是没有返回值,执行一些工作(比如资源清理)的函数,这些函数一样可以用StubFunc进行打桩,不同的是,StubFunc就不再需要第二个参数了。

比如我们有这样的函数:

package function
​
import "fmt"var ClearFunc = Clear
​
func Clear() {
    // do something
    fmt.Println("Do something...")
}

执行这样的测试代码:

func TestClear(t *testing.T) {
    stub := gostub.StubFunc(&ClearFunc)
    defer stub.Reset()
    ClearFunc()
}

可以发现使用go test -v也并没有输出Do something…,可以验证函数已经被mock掉了,如果要进一步验证可以让Clear改变一些全局变量的值,然后使用Convey里的So验证它们,比如:

package function
​
var Num int
var ClearFunc = Clear
​
func Clear() {
    // do something
    fmt.Println("Do something...")
    Num = 15
}

其测试代码:

func TestClear(t *testing.T) {
    stub := gostub.StubFunc(&ClearFunc)
    defer stub.Reset()
    Convey("After mock, Num should not be 15", t, func() {
        ClearFunc()
        So(Num, ShouldNotEqual, 15)
    })
}

测试结果是PASS的,有力地证明了ClearFunc确实被mock掉了。

四、测试框架GoMonkey

我们刚才在探究GoStub的时候也发现了,GoStub对于mock函数来说功能还是比较弱的,如果业务代码不是使用函数变量进行调用的,就要导致业务代码为了测试而进行修改。这种我们称为”侵入式“的测试。并且把这些函数变量单独拉出来作为一个适配层,也增加了代码量的冗余,总有一种”为了测试而无端增加业务代码复杂度“的感觉。

那我们就会自然而然地想到,可以不可以有一种非侵入式的,在运行时对函数进行更改的方法?我们知道,对函数变量mock本质上是因为函数在编译前是不会有地址的,所以要用函数变量的地址作为函数的地址。但是函数运行时是会有入口地址的,所以是可以通过jmp这样的汇编命令进行劫持的。所以GoMonkey就是基于这么一种通过汇编mock掉运行时函数入口的思想的一种测试框架。

首先,在使用之前,我们要先安装它:

go get "github.com/agiledragon/gomonkey"

使用前的一些注意事项:

  • gomonkey框架对于内联的函数会失效(找不到函数入口),测试的时候需要关掉所有的内联,执行go test -v时加上参数-gcflags=-l(Go 1.10以下)或者-gcflags=all=-l(Go1.10及以上)。
  • gomonkey需要运行在amd64架构上。
  • gomonkey不是并发安全的。
  • gomonkey对于私有成员方法的mock在Go 1.7版本及以上会直接抛出panic。

一般来说使用gomonkey遇到的大多数问题都可以在这四条中找到答案。

1. 利用GoMonkey对全局变量进行打桩

GoMonkey对全局变量进行打桩和GoStub基本一致,此处仅贴出语法:

// 对全局变量Num进行打桩
patch := gomonkey.ApplyGlobalVar(&Num,150)
defer patch.Reset()

2. 利用GoMonkey对函数进行打桩

GoMonkey可以直接把函数作为参数对函数进行mock,我们还是拿我们熟悉的IsOdd做例子:

package function
​
// IsOdd 判断一个数是否为奇数
func IsOdd(n int) bool {
    return n%2 != 0
}

使用gomonkey可以对IsOdd直接做替换,使用ApplyFunc进行打桩,第一个参数为函数名,第二个参数为mock函数。测试函数为:

func TestIsOdd(t *testing.T) {
    patch := gomonkey.ApplyFunc(IsOdd, func(_ int) bool {
        return true
    })
    defer patch.Reset()
    Convey("Testing IsOdd after gomonkey.ApplyFunc", t, func() {
        So(IsOdd(2), ShouldBeTrue)
    })
}

由于IsOdd很短,会被当作内联展开,我们需要显示指定禁用内联,执行:go test -v -gcflags=all=-l

结果为:
在这里插入图片描述

3. 利用GoMonkey对方法进行打桩

GoMonkey还可以对方法进行打桩,所谓方法,就是指一种type里面含有的一些函数,比如说我有这样的一个demo:

package function
​
type MyString struct {
    str string
}func (s *MyString) ReturnString() string {
    return s.str
}

在这个demo中,ReturnString就是MyString类型的一个方法。我们如果要使用gomonkey对这个方法进行mock,需要调用ApplyMethod函数,第一个参数是这个类型的反射类型,第二个参数是方法名,第三个参数是mock函数。测试函数为:

func TestMyString_ReturnString(t *testing.T) {
    var temp *MyString
    patch := gomonkey.ApplyMethod(reflect.TypeOf(temp), "ReturnString", func(_ *MyString) string {
        return "hello,world!"
    })
    defer patch.Reset()
    Convey("ReturnString should return hello,world!", t, func() {
        var test *MyString
        test = new(MyString)
        So(test.ReturnString(), ShouldEqual, "hello,world!")
    })
}

同样地,因为ReturnString会被内联,所以一样要禁用内联,执行go test -v -gcflags=all=-l得:
在这里插入图片描述

4. 利用GoMonkey对函数变量进行打桩

我们在第3章的讨论中知道,GoStub主要是对函数变量进行打桩以达到mock函数的目的的。而gomonkey本身也可以对函数变量进行打桩。其语法和GoStub完全一致,使用的函数为ApplyFuncVar。

5. 利用GoMonkey打序列桩

有时候我们可能需要执行多个测试用例,那么我们就要频繁调用Apply***函数,看起来代码非常地不简洁。gomonkey对函数、方法和函数变量都提供了一种打序列桩的方法,其名称都是原来的函数名后加Seq,比如ApplyFuncSeq, ApplyFuncVarSeq等等。此处仅举给函数打序列桩的demo,其他的可以通过类比得出。

package function
​
// IsOdd 判断一个数是否为奇数
func IsOdd(n int) bool {
    return n%2 != 0
}

其测试函数为:

func TestIsOdd(t *testing.T) {
    Convey("TestApplyFuncSeq", t, func() {
        Convey("Test first seq", func() {
            output := []gomonkey.OutputCell{
                {Values: gomonkey.Params{true}},
                {Values: gomonkey.Params{false}},
            }
            patch := gomonkey.ApplyFuncSeq(IsOdd, output)
            defer patch.Reset()
            So(IsOdd(0), ShouldEqual, true)
            So(IsOdd(0), ShouldEqual, false)
        })
    })
}

其中Params指的是被打桩的函数(在这里是IsOdd)要返回值,可以有多个,而OutputCell还有一个成员Times,表示重复多少次该mock。在打了patch之后,就可以像正常函数那样调用IsOdd并且用ShouldEqual断言是否符合预期了。

请记住一定要禁止内联(因为IsOdd真的很短)!,使用命令go test -v -gcflags=all=-l得:
在这里插入图片描述

此处再给出一个具有Times的output demo及完整测试函数:

func TestIsOdd(t *testing.T) {
    Convey("TestApplyFuncSeq", t, func() {
        Convey("Test first seq", func() {
            output := []gomonkey.OutputCell{
                {Values: gomonkey.Params{true}, Times: 2},
                {Values: gomonkey.Params{false}},
            }
            patch := gomonkey.ApplyFuncSeq(IsOdd, output)
            defer patch.Reset()
            So(IsOdd(0), ShouldEqual, true)
            So(IsOdd(8), ShouldEqual, true)
            So(IsOdd(1), ShouldEqual, false)
        })
    })
}

小结

本文总结了GoConvey,testify,GoStub和GoMonkey四种测试框架,它们在不同的场景具有不同的应用。笔者总结的是,无论什么时候都可以用Convey+So的组合优雅地实现测试用例嵌套和断言,而testify适合最基本的测试(少许测试用例)。GoStub适合给函数变量打桩,但明显地,GoMonkey可以实现GoStub的所有功能,因此在mock函数的时候推荐使用GoMonkey,比较轻量级。

本文旨在介绍不需要额外生成其他文件的测试框架,所以没有介绍对接口进行mock的测试框架GoMock,有兴趣的读者可以参考该链接:使用Golang的官方mock工具–gomock进行对接口的mock。

参考: https://zhuanlan.zhihu.com/p/168539526

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Freedom3568

技术域不存在英雄主义,不进则退

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值