前面在「Go 代码测试时怎么打桩?给大家写了几个常用案例」中我们介绍了在单元测试中使用gomonkey
为代码进行打桩的各种方法。
今天我们介绍在Go单元测试中另外一个很好用的工具库goconvey
,上面说的gomonkey
属于在 Test Double 方面提供能力,也就是我们通常说的mock
,用它们可以自定义一套实现来替换项目中的代码实现。
而goconvey
则是一个帮助我们组织和管理测试用例的框架,提供了Convey
和So
两种方法来搭配使用,支持树形结构方便构造各种场景。它本身是不会提供 mock 能力的,你可以基于goconvey
来组织你的单测,在需要mock
的场景下与gomonkey
配合使用。
本文介绍的所有内容在我的专栏《Go项目搭建和整洁开发实战》中都有更详细的实战案例练习,为大家展示怎么给项目的核心业务逻辑做基于行为驱动的BDD测试。
在项目中使用goconvey 前需要先在项目依赖中添加goconvey,安装命令如下go get github.com/smartystreets/goconvey
我们先看一下goconvey官方给出的使用示例。
package package_name
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestSpec(t *testing.T) {
// Only pass t into top-level Convey calls
Convey("Given some integer with a starting value", t, func() {
x := 1
Convey("When the integer is incremented", func() {
x++
Convey("The value should be greater by one", func() {
So(x, ShouldEqual, 2)
})
})
})
}
通过这个例子,正好说一下在使用goconvy的过程中需要注意的几个点:
import .
语法导入convey,import . "github.com/smartystreets/goconvey/convey"
,这样是为了方便大家直接使用 goconvey 中的各种定义,无需再像convey.Convey
这样加包前缀。Convey(description string, t *testing.T, action func())
Convey(description string, action func())
goconvey
为我们提供了很多种ShouldXXX
类断言方法在So()
函数中使用,来比对前后两个参数之间的关系。
func So(actual interface{}, assert Assertion, expected ...interface{}) {
mustGetCurrentContext().So(actual, assert, expected...)
}
另外如果断言失败,goconvey
底层会调用t.Fail()
方法来告诉Go
,你的go test
就会失败,所以如果使用了goconvey
,就不用在代码里手动调用t.Fail()
了。
首先需要在测试的入口 TestMain 中要加上SuppressConsoleStatistics
和PrintConsoleStatistics
,用于在测试完成后输出测试结果。
func TestMain(m *testing.M) {
// convey在TestMain场景下的入口
SuppressConsoleStatistics()
result := m.Run()
// convey在TestMain场景下的结果打印
PrintConsoleStatistics()
os.Exit(result)
}
下面我们使用goconvey 为 util 包的工具函数PasswordComplexityVerify编写测试,PasswordComplexityVerify的功能是用来检查用户注册账号时输入的密码是否满足复杂密码的要求。
package util
func PasswordComplexityVerify(s string) bool {
var (
hasMinLen = false
hasUpper = false
hasLower = false
hasNumber = false
hasSpecial = false
)
......
return hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
}
使用Convey 为他编写的测试如下:
func TestPasswordComplexityVerify(t *testing.T) {
Convey("Given a simple password", t, func() {
password := "123456"
Convey("When run it for password complexity checking", func() {
result := util.PasswordComplexityVerify(password)
Convey("Then the checking result should be false", func() {
So(result, ShouldBeFalse)
})
})
})
Convey("Given a complex password", t, func() {
password := "123@1~356Wrx"
Convey("When run it for password complexity checking", func() {
result := util.PasswordComplexityVerify(password)
Convey("Then the checking result should be true", func() {
So(result, ShouldBeTrue)
})
})
})
}
这里我们不仅仅有嵌套的 Convey,还有并列的 Convey。通过这种关系来表达各个不同测试之间的关联关系。在两个并列Convey中我们分别进行了正向和负向测试。
你可能问了,写单元测试就写呗,咋还冒出来个正向测试、负向测试呢?其实它们非常好理解:
结合我们在description
参数中的描述,我们就可以建立起来类似BDD
(行为驱动测试)的语义:
BDD测试中的描述信息通常使用的是Given、When、Then引导的状语从句,如果喜欢用中文写描述信息也要记得使用类似语境的句子。
你可能会问这么写了有什么用,咱们用命令来看看测试运行的效果,我们可以看到输出的测试结果会按照单测中Convey书写的层级,分层级显示。
goconvey
为我们提供了很多种ShouldXXX
类断言方法在So()
函数中使用,来比对前后两个参数之间的关系,主要有下面几类,大家用到的时候可以来这里参考。
So(thing1, ShouldEqual, thing2)
So(thing1, ShouldNotEqual, thing2)
So(thing1, ShouldResemble, thing2) // 用于数组、切片、map和结构体相等
So(thing1, ShouldNotResemble, thing2)
So(thing1, ShouldPointTo, thing2)
So(thing1, ShouldNotPointTo, thing2)
So(thing1, ShouldBeNil)
So(thing1, ShouldNotBeNil)
So(thing1, ShouldBeTrue)
So(thing1, ShouldBeFalse)
So(thing1, ShouldBeZeroValue)
So(1, ShouldBeGreaterThan, 0)
So(1, ShouldBeGreaterThanOrEqualTo, 0)
So(1, ShouldBeLessThan, 2)
So(1, ShouldBeLessThanOrEqualTo, 2)
So(1.1, ShouldBeBetween, .8, 1.2)
So(1.1, ShouldNotBeBetween, 2, 3)
So(1.1, ShouldBeBetweenOrEqual, .9, 1.1)
So(1.1, ShouldNotBeBetweenOrEqual, 1000, 2000)
So(1.0, ShouldAlmostEqual, 0.99999999, .0001) // tolerance is optional; default 0.0000000001
So(1.0, ShouldNotAlmostEqual, 0.9, .0001)
So([]int{2, 4, 6}, ShouldContain, 4)
So([]int{2, 4, 6}, ShouldNotContain, 5)
So(4, ShouldBeIn, ...[]int{2, 4, 6})
So(4, ShouldNotBeIn, ...[]int{1, 3, 5})
So([]int{}, ShouldBeEmpty)
So([]int{1}, ShouldNotBeEmpty)
So(map[string]string{"a": "b"}, ShouldContainKey, "a")
So(map[string]string{"a": "b"}, ShouldNotContainKey, "b")
So(map[string]string{"a": "b"}, ShouldNotBeEmpty)
So(map[string]string{}, ShouldBeEmpty)
So(map[string]string{"a": "b"}, ShouldHaveLength, 1) // supports map, slice, chan, and string
So("asdf", ShouldStartWith, "as")
So("asdf", ShouldNotStartWith, "df")
So("asdf", ShouldEndWith, "df")
So("asdf", ShouldNotEndWith, "df")
So("asdf", ShouldContainSubstring, "稍等一下") // optional 'expected occurences' arguments?
So("asdf", ShouldNotContainSubstring, "er")
So("adsf", ShouldBeBlank)
So("asdf", ShouldNotBeBlank)
So(func(), ShouldPanic)
So(func(), ShouldNotPanic)
So(func(), ShouldPanicWith, "") // or errors.New("something")
So(func(), ShouldNotPanicWith, "") // or errors.New("something")
So(1, ShouldHaveSameTypeAs, 0)
So(1, ShouldNotHaveSameTypeAs, "asdf")
So(time.Now(), ShouldHappenBefore, time.Now())
So(time.Now(), ShouldHappenOnOrBefore, time.Now())
So(time.Now(), ShouldHappenAfter, time.Now())
So(time.Now(), ShouldHappenOnOrAfter, time.Now())
So(time.Now(), ShouldHappenBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldNotHappenOnOrBetween, time.Now(), time.Now())
So(time.Now(), ShouldHappenWithin, duration, time.Now())
So(time.Now(), ShouldNotHappenWithin, duration, time.Now())
本文介绍的所有内容在我的专栏《Go项目搭建和整洁开发实战》中都有更详细的实战案例练习,为大家展示怎么给项目的核心业务逻辑做基于行为驱动的BDD测试。