1.什么是单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
对于单元测试中单元的含义,一般要根据实际情况去判定其具体含义,如 C 语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小被测功能模块。
在 Go 中,一般指对函数的单元测试。
2.单元测试的作用
单元测试可以检查我们的代码能否按照预期执行,来提升代码质量。
通过单元测试,我们可以设置多个测试用例,执行要测试的函数,判断是否符合预期。尽可能达保证函数功能没有问题,或者出现我们预知的错误。一次书写测试用例,随着代码一起永久保留,来验证函数功能,这就是单元测试的好处。
3.Go 如何写单元测试
Go 本身对自动化测试非常友好,并且有许多优秀的测试框架支持,非常好上手。
首先看一下 Go 官方的 testing 包。
要编写一个测试文件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数。将该文件放在与被测试文件相同的包中,该文件将被排除在正常的程序包之外,但在运行 go test 命令时将被包含。
测试函数的签名必须接收一个指向 testing.T 类型的指针,并且不能返回任何值,函数名必须以 Test 开头,建议后跟要测试的函数名。
记得一定要先看一下 Package testing 的官方文档!
下面利用 Go 官方 testing 包给出一个示例。
代码目录结构:
代码语言:javascript复制gotest
- go.mod
- go.sum
- main.go
- hello
- hello.go
- hello_test.go
main.go:
代码语言:javascript复制package main
import (
"main/hello"
)
func main() {
fmt.Println(hello.Hello())
}
hello.go:
代码语言:javascript复制package hello
func Hello() string {
return "Hello world"
}
我们新建一个单测文件 hello_test.go,为函数 Hello() 添加单元测试。
hello_test.go:
代码语言:javascript复制package hello
import "testing"
func TestHello(t *testing.T) {
got := Hello()
want := "Hello world"
if got != want {
t.Errorf("got %q want %q", got, want)
}
}
进入 hello 目录执行命令go test
,或指定目录 go test ./hello
,所有以_test.go
结尾的源码文件内以 Test 开头的函数会自动被执行。
输出:
代码语言:javascript复制PASS
ok main/hello 0.173s
我们也可以加上-v
选项,输出单测执行的详细过程:
go test -v
=== RUN TestHello
--- PASS: TestHello (0.00s)
PASS
ok main/hello 0.176s
该结果,表示单测通过了,返回的值与我们预期的值是相同的。
现在尝试把预期结果修改一下:
代码语言:javascript复制want := "Hello fuck"
测试结果:
代码语言:javascript复制D:codegotesthello>go test -v
=== RUN TestHello
hello_test.go:9: got "Hello world" want "Hello fuck"
--- FAIL: TestHello (0.00s)
FAIL
exit status 1
FAIL main/hello 0.127s
此时提示测试不通过,得到的值与预期的值不相同。
至此,利用 Go 官方包 testing 便完成了一个简单的单元测试,我们可以进行正确或错误的测试。
4.go test 命令参数
go test 是 Go 用来执行单元测试地命令,常用的选项有:
代码语言:javascript复制-bench regexp 执行相应的 benchmarks,例如 -bench=.;
-cover 开启测试覆盖率;
-run regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数;
-v 显示测试的详细命令。
详见官方文档Testing flags。
现在再添加一个被测试函数。
代码语言:javascript复制func Add(a, b int) int {
return a b
}
测试代码:
代码语言:javascript复制func TestAdd(t *testing.T) {
sum := Add(5, 5)
if sum == 10 {
t.Log("the result is ok")
} else {
t.Fatal("the result is wrong")
}
}
使用 -run
来测试,发现只运行了 TestAdd 测试方法。
D:codegotesthello>go test -v -run TestAdd
=== RUN TestAdd
hello_test.go:16: the result is ok
--- PASS: TestAdd (0.00s)
PASS
ok main/hello 0.170s
5.快速生成单测代码
实际上,不同函数的单测代码虽然逻辑不同,但结构是一样的,长得非常相似,因此重复的代码可以使用工具来生成,不用手动繁琐地重复书写。
常用的 IDE,比如 GoLand 或 VSCode,都自带了生成单元测试代码的工具,以 GoLand 为例,可以快速为函数、文件或包生成测试代码。在源码文件中”右键函数名 > Generate… > Test for function“ 便可以快速生成对应函数的单测代码模板,然后我们在生成的模板代码中添加具体的测试用例即可。
使用该方法,为上面的 Hello() 函数生成的测试代码如下:
代码语言:javascript复制func TestHello(t *testing.T) {
tests := []struct {
name string
want string
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Hello(); got != tt.want {
t.Errorf("Hello() = %v, want %v", got, tt.want)
}
})
}
}
我们在注释处添加测试用例即可,非常快捷方便。
6.看看单元测试覆盖率
写好测试后,可以利用 Go 自带的工具 test coverage 查看一下单元测试覆盖率。
测试覆盖率是一个术语,用于统计通过运行程序包的测试多少代码得到执行。 如果执行测试函数导致 80%的语句得到了运行,则测试覆盖率为 80%。
我们来试一下。
代码语言:javascript复制D:codegotest>go test -v -cover ./hello
=== RUN TestHello
--- PASS: TestHello (0.00s)
=== RUN TestAdd
hello_test.go:16: the result is ok
--- PASS: TestAdd (0.00s)
PASS
coverage: 100.0% of statements
ok main/hello 0.154s coverage: 100.0% of statements
可以看到,目录 hello 下的所有单测都通过了,且报告覆盖率为 100%.
7.使用单测框架写单测
学会使用 Go 官方 testing 包写单元测试是远远不够的,因为实际项目开发中,面对复杂的逻辑判断,繁多的测试用例,网络IO调用等,都加大了单测编写与管理的难度,此时我们需要用到更好的测试框架来增强测试编写。
这里推荐使用 Testify Gomonkey 开源库来完成 Go 的单元测试的书写。
Testify 主要提供测试套件和断言的能力,不过也提供了 mock 的功能,但我们不使用,因为有更好用的 mock 库。
Gomonkey 主要用于提供 mock 能力。
说到 mock,其本意是模拟,就是对一些不想执行的函数,比如有网络IO或对DB有写入的函数,因为测试环境网络不通或不想执行单测而向DB写入数据,都可以将其 mock 住,写一个替代函数。执行单测的时候会调用这个替代函数,相当于替代函数模拟了原函数。
下面使用 Testify Gomonkey 给出使用示例。
先改造一下 Hello() 和 Add() 函数。
代码语言:javascript复制// Hello 返回 Hello world 和写入 DB 的值
func Hello(a, b int) string {
return fmt.Sprintf("Hello world and %v", Add(a, b))
}
// Add 将 a b 和写入 DB 并返回
func Add(a, b int) int {
sum := a b
// 省略的 DB 操作
return sum
}
我们为 Hello() 函数添加单元测试并 mock 住 Add() 函数。
代码语言:javascript复制package hello
import (
"testing"
"github.com/agiledragon/gomonkey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
// TestSuiteHello 测试套件
type TestSuiteHello struct {
suite.Suite
patches []*gomonkey.Patches
}
// SetupTest 测试套件初始化
func (suite *TestSuiteHello) SetupTest() {
// mock Add 函数
p := gomonkey.ApplyFunc(Add, func(a, b int) int {
// 不执行DB写入,只返回加和结果
return a b
})
suite.patches = append(suite.patches, p)
}
// TearDownSuite 测试套件析构
func (suite *TestSuiteHello) TearDownSuite() {
for _, p := range suite.patches {
p.Reset()
}
}
// TestHelloSuite 启动测试套件
func TestHelloSuite(t *testing.T) {
suite.Run(t, new(TestSuiteHello))
}
// TestHello hello 函数单测
func (suite *TestSuiteHello) TestHello() {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want string
}{
{"testcase1", args{1, 2}, "Hello world and 3"},
{"testcase2", args{3, 4}, "Hello world and 7"},
{"testcase3", args{5, 6}, "Hello world and 11"},
}
for _, t := range tests {
res := Hello(t.args.a, t.args.b)
assert.Equal(suite.T(), t.want, res, t.name)
}
}
自定义的测试套件 TestSuiteHello,可以添加多个以 Test 开头的单测,将被一一执行。
执行单元测试:
代码语言:javascript复制D:codegotest>go test -v ./hello
=== RUN TestHelloSuite
=== RUN TestHelloSuite/TestHello
--- PASS: TestHelloSuite (0.00s)
--- PASS: TestHelloSuite/TestHello (0.00s)
PASS
ok main/hello 0.149s
测试通过。
8.小结
关于单元测试,本文从 0 到 1 讲解了 Go 如何编写测试用例,熟练掌握 Golang 中单元测试的书写是一位合格 gopher 的必备技能。
推荐使用 testify gomonkey 测试框架编写 Go 的单测,关于其他的单测框架,比如 goconvey gomock,感兴趣的你可自行了解。
参考文献
[1] GoLang快速上手单元测试(思想、框架、实践) [2] golang-单元测试和mock框架的介绍和推荐 [3] gomonkey 1.0 正式发布! [4] Testify - Thou Shalt Write Tests