Go单元测试学习指南
本篇文本对网上的 Go 语言单元测试的文章做了整合,面向初学者由易到难的列出了可以学习的资料,包含了 testing、mock、stub、BDD 等内容。
阅读时长:6小时 (含阅读文中的链接)
不熟悉 Go ?或者希望使用TDD?
如果你对 go 语言还不熟悉,可以直接在使用测试驱动开发(TDD)的方式学习 Go 语言。
推荐阅读 :Learn Go with tests 或 中文翻译。
入门:testing
进行单元测试最简单、功能也最弱的就是使用go标准库中自带的 testing 进行。
在这一部分,你需要学习到以下知识点(建议按顺序学习):
- 最简单的单元测试
- Table-Driven Test:使用测试表的方式在一个测试方法中运行多次不同输入输出的测试
- 查看单元测试覆盖的具体代码行(使用 go test -cover 和 go tool)
- 基准测试、性能测试(Benchmark)
- 使用 TestMain 进行额外的设置(setup)或拆卸(teardown)
这个的资料网上有非常多,下面是我挑选出来的几个不错的教程(按内容质量高低排序)。
- testing 单元测试 (相同内容的另一个链接)
- Golang UnitTest单元测试 by 黑光技术
- Golang basics writing unit tests
- golang 单元测试 UnitTest 覆盖率 基准测试 by 木猫尾巴
进阶:mock 和 stub
如果在被测函数中调用了外部函数(比如系统调用、数据库),而又不希望测试被这些的失败影响,则需要在测试函数中先对该函数打桩(stub)。
什么是 mock 和 stub?
建议阅读:
- 测试中 Fakes、Mocks 以及 Stubs 概念明晰 或 英文原版 (示例代码为Java)
- Mocks Aren’t Stubs (示例代码为Java)
- 理解测试中的stub和mock (示例代码为Python)
摘要:
- stub(桩)
- 当调用一个函数获取数据时,直接返回预定义好的数据
- For replacing a method with code that returns a specified result
- mock
- 当调用一个函数时,实际不调用而只记录发起了调用这件事,通常用于验证进行了符合期望的调用
- A stub with an expectations that the method gets called.
gomock(和mockgen)
在go语言中,进行 mock 或 stub,最“正规”的方式是使用官方提供的mock库。
mock库 包含了 mockgen 和 gomack 两部分,前者用于自动生成一些重复性代码,后者是实现 mock 或 stub 功能的部分。
下面有几篇介绍如何使用的中文文章:
gostub
gomock 非常好用,但最大的问题是,只能对接口进行mock,gostub 是一个可以对全局变量、函数进行打桩的工具,尽管给函数打桩必须修改函数的定义。
官网: https://github.com/prashantv/gostub
教程:
monkey
gostub 之所以需要修改函数的定义才能打桩,很大程度上是因为go语言是一个编译性语言,很难做到像猴子补丁一样的动态函数替换,但难≠不能。
什么是猴子补丁(monkey patch)?
将一个函数的入口点的地址在运行时进行替换,用李鬼欺骗调用者,以此实现替换一个函数,实现为函数打桩。更多信息可参考 Wikipedia。
以Python这种动态语言为例,这是一个很典型的 monkey patch,改变了标准库中 math.pi
的行为(尽管重启之后可以恢复)。
>>> import math
>>> math.pi
3.141592653589793
>>> math.pi = 3 # monkey patch the value of PI in the math module
>>> math.pi
3
对于静态语言go,其工作原理就复杂的多,我们通常不需要了解。如果感兴趣,可以参考monkey作者的这篇文章。
如何使用monkey?
这里列出一个例子,用一个自定义的函数替换了 fmt.Println
这个函数。
func main() {
monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
s := make([]interface{}, len(a))
for i, v := range a {
s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
}
return fmt.Fprintln(os.Stdout, s...)
})
fmt.Println("what the hell?") // what the *bleep*?
}
更多用法,以及常见的坑(编译器对内联函数的优化等)可参见其Github官网:monkey
常见mock/stub方法对比
功能 | gomock | gostub | monkey |
---|---|---|---|
支持对调用行为(即mock) | √ | ? | × |
对全局变量打桩 | × | √ | √ |
对接口进行打桩 | √ | ? | ×(计划中) |
对函数进行打桩 | × | √ | √ |
无需修改被测试代码 | √ | × | √ |
允许编译器优化(goinline) | √ | √ | × |
允许同一函数多次调用行为不同 | ? | × | × |
高阶:BDD风格测试
什么是BDD?
行为驱动开发(英语:Behavior-driven development,缩写BDD)是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。BDD的重点是通过与利益相关者的讨论取得对预期的软件行为的清醒认识。它通过用自然语言书写非程序员可读的测试用例扩展了测试驱动开发方法。
来源:中文Wikipedia
另可参考 Wikipedia: Behavior-driven development
Ginkgo
Ginkgo 是一个 BDD 风格的Go测试框架,旨在帮助你有效地编写富有表现力的全方位测试。
其测试代码写起来更像是自然语言(英语),示例如下:
var _ = Describe("Book", func() {
var longBook Book
BeforeEach(func() {
longBook = Book{
Title: "Les Miserables",
Pages: 1488,
}
})
Describe("Categorizing book length", func() {
Context("With more than 300 pages", func() {
It("should be a novel", func() {
Expect(longBook.CategoryByLength()).To(Equal("NOVEL"))
})
})
})
})
官网:http://onsi.github.io/ginkgo/
GoConvey
GoConvey 是另一个 BDD 风格的 Go测试框架,提供了 Web UI和实时重载功能。
其测试代码写起来和Ginkgo非常类似,示例如下:
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)
})
})
})
}
官网: https://github.com/smartystreets/goconvey
教程: https://www.jianshu.com/p/e3b2b1194830
其他工具/框架
在了解了单元测试的基本知识后,最后推荐几个好用的第三方库。
testify
https://github.com/stretchr/testify
testify 是包含了几个实用工具的集合,具体:
- assert:提供了断言一些方法,让你可以使用 assert 风格简洁地写出
if xxx { t.Fail() }
- require:同 assert,但替换为了
t.FailNow
- mock:另一个mock的实现
- suite:提供了 setup 和 teardown 方法
相关介绍文章:
gotests
gotests 是一个自动生成测试代码的小工具,可以根据选定函数的输入输出自动生成符合Table-Driven格式的测试代码,我们需要做的就只有填写测试数据了。
awesome
需要更多的单元测试工具?这份 awesome 列表请收好:https://github.com/avelino/awesome-go#testing
常见问题
如何测试 HTTP 请求?(源代码作为客户端,模拟服务端)
这里提供两种办法:
第一种,使用标准库 net/http/httptest,此种方法要求server地址是可以通过参数提供给待测函数的,允许根据需求对不同的http请求进行mock。
第二种,使用 httpmock,此种方式更改了http.DefaultTransport
变量的值,使得所有http请求全部会被mock。
第三种,使用 smocker,这是一个功能强大的 server mock,除了支持支持静态返回、用 Go templates or Lua 或生成动态返还外,还可将请求转发到真正的服务器,以实现对一些复杂内容的测试。
如何测试数据库?
对数据库的测试,大体上有两种思路,一种是使用真的数据库,另一种是通过mock不使用数据库。两种方法各有优劣。
使用真的数据库
这个要求进行单元测试的机器上是有一个真的能够访问的数据库。在测试时,需要使用前文讲到到 TestMain 中进行 setup 和 teardown 设置,以便测试代码可以重复运行。
对数据库使用mock
这里推荐 go-sqlmock库 ,这是一个实现了 sql/driver 接口的 mock库。
sqlmock只能使用正则表达式匹配SQL语句进行返回。因为不是真正的数据库,所以 sqlmock 甚至没有检查SQL语句是否存在语法错误的能力。
参考资料
除正文中的链接外,写作过程中还参考了