在看开源项目的同学,如果你已经开始注意 _test 文件了的话,那么恭喜你,你将开启单元测试的大门了。

关于单元测试,Go 语言它有一套属于自己的单元测试和性能测试系统。

相比其他语言,这块有着非常强的优势,我们只需要写很少的代码就能实现对代码的测试。

这篇文章我们主要讲解单元测试,下一篇我们再讲基准测试。

什么是单元测试?

大家可还记得我们读书时,翻开《语文》书目录的时候,总能看到第一单元,第二单元么?

像下面这张图:

图片来自网络

你会发现,一本书作者把它被分成了很多个单元,当我们学完所有单元就意味着这个学期学完了,这本书学完了。

我们的程序也是这样,我们的一个项目,大都会被分成不同的模块,不同模块里面又会有不同的单元。

当我们的每一个单元都测试 OK,也就意味着,这个项目在正常情况下使用,应该没啥问题了。

当然也有不正常的时候,那就说明卡 BUG 了,自求多福吧,哈哈。

所以在大型项目里面,单元测试是必不可少的,小项目就看项目需要了。

怎么创建?

我们只需要在我们的工程里面创建以 _test 结尾的 go 文件即可,一个单元测试文件就创建好了。

如果是在 GoLand 里面他会自动给识别,加上特殊的标记,比如下面这样的:

20220216024854_qazfsi.png

大部分 Go 语言的开源项目,这样的文件就特别多,比如这样:

20220216024904_8vawqw.png

这是 kubernetes 的 Github 截图。

开始编写

首先我们需要写一个简单的业务文件,方便我们去测试,于是我新建了一个目录 utils,然后新建一个 string.go 文件:

1
2
3
4
5
6
package utils

// JointString 拼接字符串
func JointString(a string,b string) string {
	return a+b
}

我写了一个非常简单的方法,就是拼接两个字符串。

然后我们再建一个单元测试文件。

我们喜欢把单元测试的文件名和被测试的文件关联起来,喜欢在测试的文件后面加 _test ,所以我们的文件名 就是 string_test.go 。

注意点:

文件建好之后,就需要在里面去编写我们的测试代码了。

1、其实我们的单元测试代码,和我们的普通代码没太大的区别,你正常写就好了。

2、唯一的区别就是,我们需要用到 testing 包里面的相关 API 来进行干预测试结果。

3、同时方法名必须要 Test 开头。

当然我们也比较喜欢把测试方法和被测试的方法关联起来(非硬性规定),于是我们就这样写一个最简单的测试代码:

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

import (
	"fmt"
	"testing"
)

func TestJointString(t *testing.T) {
	fmt.Println(JointString("a","b"))
}

这里我们还没用到任何的 testing 里面的 API,只是在测试方法里面调用了我们的 JointString 方法。

运行起来

如果你用的 GoLand 开发,就可以直接点击方法旁边的绿色图标即可运行:

20220216024916_cz6bjh.png

你也可以在命令行里面运行,cd 到我们的 utils 目录下面,执行 go test 即可:

1
2
3
4
5
6
7
8
9
go test -v

// 执行结果
$ go test -v
=== RUN   TestJointString
ab
--- PASS: TestJointString (0.00s)
PASS
ok      map-demo/utils  0.328s

-v 是让控制台输出测试详细的流程。

单元测试进阶

现在你已经会了基本的单元测试创建方法,接下来进阶下。

我们需要对我们的测试结果进行预判,否则只是输出一个结果那肯定达不到测试的目的。

这就需要了解下我们的 testing.T 里面的 API 了,我们常用的方法有:

方法 备注
Log 打印日志,同时结束测试
Logf 格式化打印日志,同时结束测试
Error 打印错误日志,同时结束测试
Errorf 格式化打印错误日志,同时结束测试
Fatal 打印致命日志,同时结束测试
Fatalf 格式化打印致命日志,同时结束测试

接下来我们进行改造我们的测试方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func TestJointString(t *testing.T) {
	if JointString("a","b")!="ab" {
		t.Error("与我们预判的结果不一致")
	}
	// 故意写一个错误的预判
	if JointString("g","b")!="gob" {
		t.Error("与我们预判的结果不一致")
	}
}

//执行结果
$ go test  -v
=== RUN   TestJointString
    string_test.go:13: 与我们预判的结果不一致
--- FAIL: TestJointString (0.00s)
FAIL
exit status 1
FAIL    map-demo/utils  1.851s

你会发现测试结果就是 FAIL 了。

再次进阶

我们一般对一个方法不会是只有一两个预判。

如果有 N 个预判,你还继续使用上面这种 if 来判断,会有点 LOW。

于是我们可以对我们的测试代码再次升级。

我们可以模拟一个把测试数据和预判结果放到一个二维数组里面来 for 遍历,这种方式我们也叫  表驱动测试模式 。

上代码:

 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
func TestJointString1(t *testing.T) {
	type args struct {
		a string
		b string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		// 这里编写我们的预判逻辑
		{"testA", args{"d","v"}, "dv"},
		{"testB", args{"c","d"}, "cd"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := JointString(tt.args.a, tt.args.b); got != tt.want {
				t.Errorf("JointString() = %v, want %v", got, tt.want)
			}
		})
	}
}

// 执行结果
$ go test  -v
=== RUN   TestJointString1
=== RUN   TestJointString1/testA
=== RUN   TestJointString1/testB
--- PASS: TestJointString1 (0.00s)
    --- PASS: TestJointString1/testA (0.00s)
    --- PASS: TestJointString1/testB (0.00s)
PASS
ok      map-demo/utils  0.437s

我们每次只需要调整我们中间的预判逻辑即可。

你学废了么?

欢迎加入我们的交流群一起讨论。

只需要在公众号里面回复 加群 即可加入到我们的交流群,快来吧!