0%

Testing in Go

Test 测试

单元测试

单元测试一般是用来测试我们的代码逻辑有没有问题,有没有按照我们期望的运行,以保证代码质量。

大多数的单元测试,都是对某一个函数方法进行测试,以尽可能的保证没有问题或者问题可被我们预知。为了达到这个目的,我们可以使用各种手段、逻辑,模拟不同的场景进行测试。

1
2
3
4
// add.go
func Add(a, b int) int {
return a + b
}
1
2
3
4
5
6
7
8
9
// add_test.go
func TestAdd(t *testing.T) {
sum := Add(1,2)
if sum == 3 {
t.Log("the result is ok")
} else {
t.Fatal("the result is wrong")
}
}

然后我们在终端的项目目录下运行go test -v就可以看到测试结果了。

1
2
3
4
5
6
$ go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
add_test.go:26: the result is ok
PASS
ok 0.007s

Go语言为我们提供了测试框架,以便帮助我们更容易的进行单元测试,但是要使用这个框架,需要遵循如下几点规则:

  1. 含有单元测试代码的go文件必须以_test.go结尾,Go语言测试工具只认符合这个规则的文件
  2. 单元测试文件名_test.go前面的部分最好是被测试的方法所在go文件的文件名,比如例子中是add_test.go,因为测试的Add函数,在add.go文件里
  3. 单元测试的函数名必须以Test开头,是可导出公开的函数
  4. 测试函数的签名必须接收一个指向testing.T类型的指针,并且不能返回任何值
  5. 函数名最好是Test+要测试的方法函数名,比如例子中是TestAdd,表示测试的是Add这个这个函数

测试覆盖率

我们尽可能的模拟更多的场景来测试我们代码的不同情况,但是有时候的确也有忘记测试的代码,这时候我们就需要测试覆盖率作为参考了。

由单元测试的代码,触发运行到的被测试代码的代码行数占所有代码行数的比例,被称为测试覆盖率,代码覆盖率不一定完全精准,但是可以作为参考,可以帮我们测量和我们预计的覆盖率之间的差距,go test工具,就为我们提供了这么一个度量测试覆盖率的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tag.go
func Tag(tag int){
switch tag {
case 1:
fmt.Println("Android")
case 2:
fmt.Println("Go")
case 3:
fmt.Println("Java")
case 4:
fmt.Println("Python")
default:
fmt.Println("C")
}
}
1
2
3
4
5
// tag_test.go
func TestTag(t *testing.T) {
Tag(1)
Tag(2)
}

现在我们使用go test工具运行单元测试,和前几次不一样的是,我们要显示测试覆盖率,所以要多加一个参数-coverprofile,所以完整的命令为:go test -v -coverprofile=c.out-coverprofile是指定生成的覆盖率文件,例子中是c.out,这个文件一会我们会用到。现在我们看终端输出,已经有了一个覆盖率。

1
2
3
4
5
6
7
8
$ go test -v -coverprofile=c.out
=== RUN TestTag
Android
Go
--- PASS: TestTag (0.00s)
PASS
coverage: 50.0% of statements
ok test 0.001s

coverage: 50.0% of statements,50%的测试覆盖率,还没有到100%,那么我们看看还有那些代码没有被测试到。这就需要我们刚刚生成的测试覆盖率文件c.out生成测试覆盖率报告了。生成报告有go为我们提供的工具,使用go tool cover -html=c.out -o=tag.html,即可生成一个名字为tag.html的HTML格式的测试覆盖率报告,这里有详细的信息告诉我们哪一行代码测试到了,哪一行代码没有测试到。

从上图中可以看到,标记为绿色的代码行已经被测试了;标记为红色的还没有测试到,有2行的,现在我们根据没有测试到的代码逻辑,完善我的单元测试代码即可。

1
2
3
4
5
6
7
func TestTag(t *testing.T) {
Tag(1)
Tag(2)
Tag(3)
Tag(4)
Tag(6)
}

Benchmark测试

基准测试,是一种测试代码性能的方法,比如你有多种不同的方案,都可以解决问题,那么到底是那种方案性能更好呢?这时候基准测试就派上用场了。

基准测试主要是通过测试CPU和内存的效率问题,来评估被测试代码的性能,进而找到更好的解决方案。比如链接池的数量不是越多越好,那么哪个值才是最优值呢,这就需要配合基准测试不断调优了。

基准测试

基准测试代码的编写和单元测试非常相似,它也有一定的规则,我们先看一个示例。

1
2
3
4
5
6
7
8
// itoa_test.go
func BenchmarkSprintf(b *testing.B){
num:=10
b.ResetTimer()
for i:=0;i<b.N;i++{
fmt.Sprintf("%d",num)
}
}

这是一个基准测试的例子,从中我们可以看出以下规则:

  1. 基准测试的代码文件必须以_test.go结尾
  2. 基准测试的函数必须以Benchmark开头,必须是可导出的
  3. 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
  4. 基准测试函数不能有返回值
  5. b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰
  6. 最后的for循环很重要,被测试的代码要放到循环里
  7. b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能

下面我们运行下基准测试,看看效果。

1
2
3
4
$ go test -bench=. -run=none
BenchmarkSprintf-8 20000000 117 ns/op
PASS
ok flysnow.org/hello 2.474s

运行基准测试也要使用go test命令,不过我们要加上-bench=标记,它接受一个表达式作为参数,匹配基准测试的函数,.表示运行所有基准测试。

因为默认情况下 go test 会运行单元测试,为了防止单元测试的输出影响我们查看基准测试的结果,可以使用-run=匹配一个从来没有的单元测试方法,过滤掉单元测试的输出,我们这里使用none,因为我们基本上不会创建这个名字的单元测试方法。

下面着重解释下说出的结果,看到函数后面的-8了吗?这个表示运行时对应的GOMAXPROCS的值。接着的20000000表示运行for循环的次数,也就是调用被测试代码的次数,最后的117 ns/op表示每次需要话费117纳秒。

以上是测试时间默认是1秒,也就是1秒的时间,调用两千万次,每次调用花费117纳秒。如果想让测试运行的时间更长,可以通过-benchtime指定,比如3秒。

1
2
3
4
$ go test -bench=. -benchtime=3s -run=none
BenchmarkSprintf-8 50000000 109 ns/op
PASS
ok flysnow.org/hello 5.628s

性能对比

上面那个基准测试的例子,其实是一个int类型转为string类型的例子,标准库里还有几种方法,我们看下哪种性能更加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func BenchmarkSprintf(b *testing.B){
num:=10
b.ResetTimer()
for i:=0;i<b.N;i++{
fmt.Sprintf("%d",num)
}
}

func BenchmarkFormat(b *testing.B){
num:=int64(10)
b.ResetTimer()
for i:=0;i<b.N;i++{
strconv.FormatInt(num,10)
}
}

func BenchmarkItoa(b *testing.B){
num:=10
b.ResetTimer()
for i:=0;i<b.N;i++{
strconv.Itoa(num)
}
}

运行基准测试,看看结果

1
2
3
4
5
6
$ go test -bench=. -run=none              
BenchmarkSprintf-8 20000000 82.8 ns/op
BenchmarkFormat-8 500000000 3.06 ns/op
BenchmarkItoa-8 300000000 4.78 ns/op
PASS
ok test 5.508s

从结果上看strconv.FormatInt函数是最快的,其次是strconv.Itoa,然后是fmt.Sprintf最慢,前两个函数性能达到了最后一个的3倍多。那么最后一个为什么这么慢的,我们再通过-benchmem找到根本原因。

1
2
3
4
5
6
$ go test -bench=. -run=none              
BenchmarkSprintf-8 20000000 83.5 ns/op 16 B/op 2 allocs/op
BenchmarkFormat-8 500000000 3.08 ns/op 0 B/op 0 allocs/op
BenchmarkItoa-8 300000000 4.73 ns/op 0 B/op 0 allocs/op
PASS
ok test 5.518s

-benchmem可以提供每次操作分配内存的次数,以及每次操作分配的字节数。从结果我们可以看到,性能高的两个函数,每次操作都是进行0次内存分配,而最慢的那个要分配2次;性能高的每次操作分配2个字节内存,而慢的那个函数每次需要分配0字节的内存。从这个数据我们就知道它为什么这么慢了,内存分配都占用都太高。

Example测试

子测试

Main测试

HTTP测试