前面在「Go 代码测试时怎么打桩?给大家写了几个常用案例」中我们介绍了在单元测试中使用gomonkey
为代码进行打桩的各种方法。
今天我们介绍在Go单元测试中另外一个很好用的工具库goconvey
,上面说的gomonkey
属于在 Test Double 方面提供能力,也就是我们通常说的mock
,用它们可以自定义一套实现来替换项目中的代码实现。
而goconvey
则是一个帮助我们组织和管理测试用例的框架,提供了Convey
和So
两种方法来搭配使用,支持树形结构方便构造各种场景。它本身是不会提供 mock 能力的,你可以基于goconvey
来组织你的单测,在需要mock
的场景下与gomonkey
配合使用。
本文介绍的所有内容在我的专栏《Go项目搭建和整洁开发实战》中都有更详细的实战案例练习,为大家展示怎么给项目的核心业务逻辑做基于行为驱动的BDD测试,专栏中还有更多实战课题,扫码加入一起成长吧。
goconvey 的安装和基本用法
在项目中使用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 函数是可以嵌套的,这样我们就可以构造出来一条测试的行为路径,帮助我们写出BDD风格的单测。
Convey 嵌套使用时函数的入参有区别
最上层Convey 为
Convey(description string, t *testing.T, action func())
其他层级的嵌套 Convey 不需要传入 *testing.T,为
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()
了。
goconvey 实战演示
TestMain设置
首先需要在测试的入口 TestMain 中要加上SuppressConsoleStatistics
和PrintConsoleStatistics
,用于在测试完成后输出测试结果。
func TestMain(m *testing.M) {
// convey在TestMain场景下的入口
SuppressConsoleStatistics()
result := m.Run()
// convey在TestMain场景下的结果打印
PrintConsoleStatistics()
os.Exit(result)
}
BDD行为驱动测试实战
下面我们使用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
(行为驱动测试)的语义:
Given【给定某些初始条件】
Given a simple passowrd 给定一个简单密码
When 【当一些动作发生时】
When run it for password complexity checking 当对它进行复杂度检查时
Then 【结果应该是】
Then the checking result should be false 结果应该是 false
BDD测试中的描述信息通常使用的是Given、When、Then引导的状语从句,如果喜欢用中文写描述信息也要记得使用类似语境的句子。
你可能会问这么写了有什么用,咱们用命令来看看测试运行的效果,我们可以看到输出的测试结果会按照单测中Convey书写的层级,分层级显示。

GoConvey提供的断言方法
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)
panic类
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测试,专栏中还有更多实战课题。扫码订阅专栏,加入队伍一起成长吧。
专栏分为五大部分,重点章节如下
第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项
扫描上方二维码或者访问 https://xiaobot.net/p/golang 即刻订阅
此外想更详细地了解专栏内容,或者希望专栏有优惠时能提前告知你,都可以添加下面我的微信
