Golang 如何进行单元测试

在 golang 中,如果有一个 practice.go 的文件,那么在后面加上 _test(practice_test.go) 就是对应的单元测试文件。当构建程序时,名为 xxx_test.go 的文件会被编译器忽略。一般来说,单元测试的文件和被测试的文件放在同一个目录下,单元测试文件也是 package 的一部分。

第一个单元测试

1
2
3
4
5
6
7
package greeting

import "fmt"

func sayHello(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}

对应的测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
package greeting

import "testing"

func TestSayHello(t *testing.T) {
expected := "Hello, Andy!"
actual := sayHello("Bob")
if expected != actual {
t.Errorf("Actual %s does not match expected %s", actual, expected)
}
}

单元测试的方法签名分为两部分,第一部分是固定的 Test,第二部分则是被测试的方法名,且必须是大写字母开头。

单元测试没有标记成功的方法,当测试方法没有调用任何失败方法时,就意味着测试成功。要标记失败,有如下几种方式:

  • Error,记录日志,标记单元测试方法失败,测试将继续
  • Errorf,同上,日志按照指定格式记录
  • Fail,标记单元测试方法失败,测试将继续
  • FailNow,标记测试失败,将不会继续执行
  • Fatal,同FailNow + 记录日志
  • Fatalf,同FailNow + 指定格式记录日志

如果需要使用额外的文件,如配置文件等,将其放在当前目录的 testdata 文件夹下。

以上的单元测试使用的是标准库的方法,也可以引入开源库来做断言处理。

1
go getgithub.com/stretchr/testify

上面的测试代码就可以改写成:

1
2
3
4
5
6
7
8
9
10
package greeting

import (
"testing"
"github.com/stretchr/testify/assert"
)

func TestSayHello(t *testing.T) {
assert.Equal(t, "Hello, Andy!", sayHello("Bob"), "Not equal")
}

执行单元测试

只需 cd 到对应文件的目录下,执行go test 即可执行单元测试,这个命令会执行当前目录下 package 中的所有测试用例。如果想要执行整个项目中的所有测试用例,可以使用 go test ./...

go test 执行的过程中,go 也会在 package 中自动执行 go vetgo vet命令是 Go 工具链的一部分。它可以对源代码进行语法验证,以检测潜在的错误。这个命令有一个完整的检查列表,但是在运行 go 测试时,只启动一个小的检测列表子集。

  • atomic:检测出 sync/atomic 包的错误使用。
  • bool:验证布尔条件的使用情况。
  • buildtags:验证在输入的命令中go test是否正确拼装了 build 标签。
  • nilfunc:检查是否将函数与nil进行比较。

在启动单元测试之前自动运行go vet命令可以使你在对程序造成影响之前就发现这些错误。

多个测试用例

如果一个方法有多个测试用例需要去测试,那么该如何去写呢?最容易想到的肯定是直接复制然后修改。其实可以把测试用例参数化,具体来看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

func TestSayHello(t *testing.T) {
type args struct {
name string
}
tests := []struct {
name string
args args
want string
}{
{
name: "test Bob",
args: args{"Bob"},
want: "Hello, Bob!",
},
{
name: "test Andy",
args: args{"Andy"},
want: "Hello, Andy!",
},
{
name: "Mike",
args: args{"Mike"},
want: "Hello, Mike!",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := sayHello(tt.args.name); got != tt.want {
t.Errorf("sayHello() = %v, want %v", got, tt.want)
}
})
}
}

首先,创建一个名为 argsstruct。每一个字段都代表待测试方法中的参数。
然后再创建一个 testsstruct,这里有三个字段:

  • name,即该测试用例的名字,一般取个比较易读易理解的名字。
  • args,传递给被测试方法的参数。
  • want,期望执行方法的返回值。

接下来的循环中,则是遍历执行测试用例了。这样添加或删除测试用例就方便多了。

并行测试

如果项目中测试用例太多或者耗时太长,就需要引入并行测试了。

新添加三个测试方法,每个方法不做其它事情,只是单纯地 sleep 500毫秒。

1
2
3
4
5
6
7
8
9
10
11
func TestGreeting1(t *testing.T) {
time.Sleep(time.Millisecond * 500)
}

func TestGreeting2(t *testing.T) {
time.Sleep(time.Millisecond * 500)
}

func TestGreeting3(t *testing.T) {
time.Sleep(time.Millisecond * 500)
}

执行测试,终端输出:

1
2
PASS
ok greeting 2.248s

可以看到,耗时是相当长的。此时只需要在每个测试方法里面加上t.Parallel(),再次运行:

1
2
PASS
ok greeting 0.937s

测试时间成功变少了,而仅仅只是添加了一句代码而已。

测试覆盖率

go test 添加 -cover 参数可以查看测试覆盖率,也可以通过 go test -coverprofile profile 生成报告。报告内容如下所示:

1
2
3
4
5
mode: set
golib/greeting.go:5.35,7.2 1 1
golib/greeting.go:9.19,9.20 0 0
golib/greeting.go:10.19,10.20 0 0
golib/greeting.go:11.19,11.20 0 0

这个文件并不易读。第一行先忽略,从第二行开始看。首先是文件路径,5.35代表了测试块的起始行列,7.2代表了测试块的结束行列。接下来的两个数字分别表示总的语句数量和测试覆盖到的语句数量。

由于此文件并不清晰易读,可以使用 go tool cover -html=profile 生成一个 html 文件并在浏览器中打开。