6.程序测试和文档
6.1程序测试
Go语言中提供了 go test 命令,它不仅仅可以对代码包进行测试,还可以对个别源码文件进行测试,只要存在针对这些测试的测试源码文件。除此之外,Go语言还在标准库中提供了一个专门用于测试的代码包 testing,它提供了编写测试源码文件所需的一切。
1.功能测试
测试源码文件总应该与被它测试的源码文件处于同一代码包内。在编写测试源码文件的时候,总是会用到标准库代码包 testing 中的 API。testing 包为Go语言的代码包提供了自动化测试支持。它的目标是与 go test 命令协同使用,以自动执行目标代码包中的任何测试函数。
(1).编写功能测试函数
在测试源码文件中,针对其他源码文件中的程序实体的功能测试程序总是以函数为单位的。被用于测试程序实体功能的函数的名称和签名形如:
func TestXxx(t *testing.T)
其中,Xxx 应该是大写字母开头的若干字母或数字的组合,通常情况下会将 Xxx 替换成被测试的程序实体的名称。可以利用 *testing.T 类型的参数 t 上的一些方法对功能测试的过程进行记录和控制。使用t的值上的方法记录的信息会在测试结束之后(不论成败)一并打印到标准输出上。
(2)常规记录
参数 t 上的 Log 和 Logf 方法一般用于记录一些常规信息,以展示测试程序的运行过程以及被测试程序实体的实时状态。调用语句如下:
t.Log("Tomorrow is a ", "good ", "day ")//类似于fmt.Println
t.Logf("Tomorrow is a %s", " good day ")//类似于fmt.Printf
使用 go test –v 命令,两者都会打印如下信息:
xxx_test.go:10: Tomorrow is a good day
xxx_test.go:11: Tomorrow is a good day
xxx_test.go:10: 代表了调用语句所在的测试源码文件的名称以及出现的行号。
(3).错误记录
参数 t 上的 Error 和 Errorf 方法被用于错误信息。当被测试的程序实体的状态不正确的时候,使用 t.Error 或 t.Errorf 方法,及时对当前的错误状态进行记录。例如:
actLen := len(s)
if actLen != expLen {
t.Errorf("Error: The length of slice should be %d but %d.\n", expLen, actLen)
}
调用 t.Error 方法相当于先后对 t.Log 和 t.Fail 方法进行调用,而调用 t.Errorf 方法则相当于与先后对 t.Logf 和 t.Fail 方法进行调用。
(4).致命错误记录
参数 t 上的 Fatal 和 Fatalf 方法被用于记录致命的被程序实体的状态错误。所谓致命错误是指使得测试无法继续进行的错误。例如:
if listener == nil {
t.Fatalf("Listener startup failing! (addr=%s)!\n", serverAddr)
}
调用 t.Fatal 方法相当于先后对 t.Log 和 t.FailNow 方法进行调用,而调用 t.Fatalf 方法则相当于先后对 t.Logf 和 t.FailNow 方法进行调用。
(5).失败标记
如果需要标记当前测试函数中的测试是失败的,那么就需要用到 t.Fail 方法。对 t.Fail 方法的调用不会终止当前测试函数的执行。但是,此函数的测试结果已经被标记为失败了。
(6).立即失败标记
方法 t.FailNow 与 t.Fail 不同的地方是,它在被调用时会立即终止当前测试函数的执行。这会使得当前的测试运行程序转而去执行其他的测试函数。
注意:只能在运行测试函数的 Goroutine 中调用 t.FailNow 方法,而不能在测试代码创建出的 Goroutine 中调用它。不过,在其他的 Goroutine 中调用 t.FailNow 方法也不会造成什么错误,只是它不会产生任何效果而已。
(7).失败判断
在调用 t.Failed 方法之后,会获得一个 bool 类型的结果值,它代表了当前的测试函数中的测试是否已被标记为失败。
(8).忽略测试
调用 t.SkipNow 方法目的是标记当前测试函数为已经被忽略,并且立即终止该函数的执行,当前的测试运行程序会转而去执行其他测试函数。与 t.FailNow 方法相同,t.SkipNow 方法也只能在运行测试函数的 Goroutine 中被调用。
调用 t.Skip 方法相当于先后对 t.Log 和 t.SkipNow 方法进行调用,而调用 t.Skipf 方法则相当于先后对 t.Logf 和 t.SkipNow 方法进行调用。
方法 t.Skipped 的结果值会告知当前的测试是否已被忽略。
(9).并行运行
方法 t.Parallel 的调用会使当前的测试函数被标记为可并行运行的。这会使测试运行程序可以并发地执行它以及其他可并行运行的测试函数。
(10).功能测试的运行
使用 goc2p 项目的代码包 cnet/ctcp 和 pkgtool 为例,如下是下载地址:
https://github.com/hyper-carrot/go_command_tutorial
该代码包中仅包含一个名为 tcp_test.go 的测试源码文件。该测试源码文件包含了两个测试函数。一个是名为 TestPrimeFuncs 的功能测试函数,一个是名为 BenchmarkPrimeFuncs 的基准测试函数。
使用 go test 命令运行 cnet/ctcp 包中的测试结果如下截图:
如果只想运行代码包中部分测试的话,有两种方式可以选择:
第一种就是 go test 命令后面以测试源码文件及其测试的源码文件为参数,而不是代码包。例如:
go test envir_test.go envir.go
第二种方式是使用标记 -run 。-run 标记的值应该为一个正则表达式。名称与此正则表达式匹配的功能测试函数,才会在当次的测试运行过程中被执行。运行截图如下:
该代码包的测试源码文件 tcp_test.go 中的功能测试函数 TestPrimeFuncs 会被执行。但当正则表达式改为“Prima”后,由于没有 cnet/ctcp 包并没有名称与之匹配的功能测试函数。例如:
在Go语言中,可以通过方法 t.Log 和 t.Logf 来记录测试过程。但是,在默认情况下,使用此方法打印的信息不会被显示出来的。因此,需要标记 -v , -v 作用是在测试运行结束后打印出所有在测试过程中被记录的日志。出于测试的考虑,强烈建议在测试源码文件中使用方法参数t的值上的方法来记录日志。
再看如下的一条示例,同时测试代码包 cnet/ctcp 和代码包 pkgtool,如下运行截图:
(11).关于测试运行的时间
现在考虑这样一种测试场景,在一个测试函数包含一段了耗时较长的代码,并且需要严格规定执行这个测试函数的耗时上限。可以在执行 go test 命令时加入标记 -timeout,且在达到其值所代表的时间上限时测试还未结束,那么就会引发一个运行时恐慌。-timeout 标记的值是类型 time.Duration 可以接受的时间表示法。例如,1h20s代表1小时20秒,2h45m 代表2小时45分钟,200ms代表200毫秒。
有效的时间单位
时间单位 | 字符串表示法 |
---|---|
纳秒 | “ns” |
微秒 | “us”或“µs” |
毫秒 | “ms” |
秒 | “s” |
分钟 | “m” |
小时 | “h” |
之前运行代码包 cnet/ctcp 中的功能测试函数的执行耗时大约2秒左右。现在通过 -timeout 标记将测试耗时上限设置为100毫秒,并运行测试。如下:
E:\Software\Go\goc2p\src>go test -timeout 100ms cnet/ctcp
panic: test timed out after 100ms
……
FAIL cnet/ctcp 0.715s
如果只是想让测试尽快结束,使用 -short 标记意味着之后要运行的测试尽量缩短它们的运行时间。代码包 testing 中有一个名为 Short 的函数。这个函数在被调用后会返回一个类型 bool 的值。这个值表明了是否在执行 go test 命令的时候加入了 -short 标记。如果这个函数返回的 bool 值为 true ,那么就可以根据具体情况,去剪裁测试代码从而缩短测试运行时间了。可以在一个功能测试函数中写一段类似的代码:
if testing.Short() {
multiSend(serverAddr, "SenderT", 1, (2 * time.Second), showLog)
} else {
multiSend(serverAddr, "SenderT1", 2, (2 * time.Second), showLog)
multiSend(serverAddr, "SenderT2", 1, (2 * time.Second), showLog)
}
这段代码来自测试源码文件 tcp_test.go 中的测试函数 TestPrimeFuncs,但做了修改,关注点放在了函数 multiSend 上,根据 testing.Short() 的返回的结果值做了不同的策略。
(12).测试的并发执行
如果功能测试运行在拥有多核CPU或者多CPU的计算机上,那么可以使用并发的方式来执行测试。通过 -parallel 标记,能够设置允许并发执行的功能测试函数的最大数量。但能够成为被并发执行的功能测试函数需要具备一个先决条件:在功能测试函数的开始处加入代码 t.Parallel() 。在调用 t.Parallel 方法的时候,执行功能测试函数的测试运行程序会阻塞在这里,并等待其他同样满足并发执行条件的测试函数。当所有需要并行执行的测试函数都被清点且阻塞后,命令程序会根据 -parallel 标记的值,全部或者部分地并发执行这些功能测试函数中的在语句 t.Parallel() 之后的那些代码。
-parallel 标记的默认值是通过标准库的代码包 runtime 的函数 GOMAXPROCS 设置的值。该函数的作用是设置Go语言并发处理的最大数量。实际上,即使 -parallel 标记的值大于这个Go语言最大并发处理数,真正能够并发执行的功能测试函数的数量也不会比它多,所以在通常情况下,并不需要在命令中加入 -parallel 标记,让它的实际值为默认值就好了。但需要注意的是,Go语言最大并发处理数的默认值为 1 。如果想要某些测试函数中的代码被并发地执行,要做的就是在测试源码文件的 init 函数中设置适当的Go语言最大并发处理数,并在这些测试函数中加入语句 t.Parallel()。
2.基准测试
所谓基准测试(Benchmark Test,简称BMT)是指,通过一些科学的手段实现对一类测试对象的某项性能指标进行可测量、可重复和可比对的测试。很多时候,基准测试已被狭义地称为性能测试。
(1).编写基准测试函数
与功能测试相同,针对其他源码文件中的程序实体的基准测试程序也是以测试函数为单位的。一个基准测试函数的名称和签名如下:
func BenchmarkXxx(b *testing.B)
(2).关于计时器
在 *testing.B 类型中,与定时器相关的方法有3个它们是 StartTimer、StopTimer 和 ResetTimer 。这3个方法被用于操纵基准测试函数的计时器。该计时器的作用是计算当前基准测试函数的执行时间。
调用 b.StartTimer 方法意味着开始对当前的测试函数的执行进行计时。它总会在开始执行基准测试函数的时候被自动地调用。这个方法被暴露出来的意义在于:计时器在被停止之后重新启动。调用 b.StopTimer 方法可以使当前测试函数的计时器停止。例如:
package bmt
import (
"testing"
"time"
)
func Benchmark(b *testing.B) {
customTimerTag := false
if customTimerTag {
b.StopTimer()
}
b.SetBytes(12345678)
time.Sleep(time.Second)
if customTimerTag {
b.StartTimer()
}
}
如上文件命名为 bmt_test.go 存放到工作区的 testing/bmt 代码包中,运行基准测试截图如下:
现在将其中的 customTimerTag 变量的值改为 true ,再来运行测试,截图如下:
从上面两个运行截图可以看见最后两行的输出内容不同。在第二个截图中,倒数第二行的3个部分分别代表了当前测试函数的名称,操作次数以及操作平均耗时。其中,操作次数是当前的基准测试函数被执行的次数,而操作平均耗时是当前基准测试函数的平均执行时间。
同样观察两个运行截图的倒数第二行可知,当 customTimerTag 为 true 时,基准测试函数 Benchmark 可以被执行多次,而当 customTimerTag 为 false 时,它往往只能获得一次执行机会。这些都是由于 testing 包中有这样的一个限制:在基准测试函数单次执行时间超过指定值(默认为1秒,也可以由标记 -benchtime 自定义)的情况下,只执行该基准测试函数一次。也就是测试运行程序会在不超过这个执行时间上限的情况下尽可能多次地执行一个基准测试函数。
当 customTimerTag 为 true 时,在调用语句 time.Sleep(time.Second) 的之前和之后,分别停止和重启了 Benchmark 函数的计时器,这就相当于不把 time.Sleep(time.Second) 语句的执行时间算在 Benchmark 函数的执行时间之内,执行 Benchmark 函数的时间已经基本可以忽略不计了(可以从 0.00 ns/op 可知),这样测试运行程序在 Benchmark 函数的累积执行时间为到达时间上限之前就会连续不断地重复执行它。
当 customTimerTag 为 false 时,调用语句 time.Sleep(time.Second) 让当前的测试程序“休息”1 秒,Benchmark 函数的单次执行时间就肯定会大于 1 秒。因此测试运行程序就不会对 Benchmark 函数执行第二次。
对于方法 b.ResetTimer 在被调用时,会重置当前基准测试函数的计时器,就是把该函数的执行时间重置为 0,这相当于把当前函数中在 b.ResetTimer 语句之前的所有语句的执行时间都从该函数的执行时间中减去。
(3).关于内存分配统计
方法 b.ReportAllocs 的含义是判断在启动当前测试的 go test 命令的后面是否有 -benchmem 标记。它会返回一个 bool 类型的结果值。
方法 b.SetBytes 接受一个 int64 类型的值,它被用于记录在单次操作中被处理的字节的数量。
当 customTimerTag 为 false,运行截图中,针对 Benchmark 函数的操作信息的那一行信息中多处了一个部分—— 12.34MB/s 。它的含义是每秒被处理的字节的数量(以 MB 为单位)。这个数量其实等于测试运行程序在执行(可能是多次) Benchmark 函数的过程中每秒调用 b.SetBytes 方法的次数乘以传入的那个整数。
首先试想一个场景:在基准测试函数 Benchmark 中测试的是一个向文件系统中写入数据的函数。在写入成功后,会调用 b.SetBytes 方法并把真正写入的字节数作为参数传入。通过测试结果信息中的 xxx MB/s ,可以获知该函数每秒能向文件系统写入多少兆字节( MB )的数据了。
从上面总结,b.SetBytes 方法能够从输入输出(IO)的角度统计出被测试的程序实体的实际性能。
(4).基准测试的运行
在上面的测试中,go test 命令只运行了 cnet/ctcp 包中的功能测试。下面说说 go test 命令的基准测试标记说明。
标记名称 | 标记描述 |
---|---|
-bench regexp | 在默认情况下,go test命令不会运行任何基准测试,但可以使用该标记以执行匹配“regexp”处的 正则表达式的基准测试函数,“regexp”可以被替换成任何正则表达式。如果需要运行所有的基准测试函数, 添加 –bench . 或 –bench=. 或 –bench=“.” |
-benchmem | 在输出内容中包含基准测试的内存分配统计信息 |
-benchtime t | 用来间接地控制单个基准测试函数的操作次数。这里的“t”指的是执行单个测试函数的累积耗时上限。 “t”处的内容使用的是类型time.Duration可接受的时间表示法。“t”的默认值是1s |
运行针对代码包 cnet/ctcp 运行基准测试的截图如下:
结构体类型 testing.B 的字段 N 可以被用来设置对基准测试函数中的某一个代码块的重复执行次数。例如:
for i := 0; I < b.N; i++ {
//测试代码
}
运行截图如下:
对于计算 N 的值的具体算法,可以查看标准库的 testing 包的源码文件 benchmark.go 中的相关代码。
如果要看到基准测试函数的操作次数和操作平均耗时的同时获得这个过程中的内存分配情况,就需要用到 -benchmem 标记。例如下面截图:
“23416 B/op”是每次操作分配的字节的平均数为23416个。“109 allocs/op”是每次操作分配内存的次数平均为109次。
go test命令还可以接受一个可自定义测试运行次数并在测试运行期间改变Go语言最大并发处理数的标记 -cpu , -cpu 标记可以是一个整数列表,多个整数之间用逗号分隔。-cpu 标记的处理方式和 -parallel 标记相反,-parallel 标记默认使用Go语言最大并发处理数,而 -cpu 标记却会直接设置它。但是,由 -cpu 标记引发的Go语言最大并发处理数的设置操作并不会影响 -parallel 标记的默认值。因为 -parallel 标记的值是在测试运行程序初始化的时候设置的。如果在 go test 命令中没有显式地加入 -parallel 标记,则它的值会被设置为测试运行程序初始化时刻的Go语言最大并发处理数。在这个时刻,测试程序运行还没有把 -cpu 标记的值(如果有的话)解析成整数数组,也就无法使用这个数组中的整数设置Go语言最大并发处理数了。
使用 -cpu 标记运行截图如下:
测试运行程序执行基准测试函数 BenchmarkPrimeFuncs 的次数是 7,这与 -cpu 标记的值1,2,4,8,12,16,20中的 7 个数字相对应。上面只是展示了基准测试的运行记录,同样这边也调用了 7 次功能测试函数 TestPrimeFuncs 。
如上运行截图中,倒数 行的末尾包含了一行运行时环境信息:[GOMAXPROCS=20, NUM_CPU=4, NUM_GOROUTINE=2],对于第一个 GOMAXPROCS 代表Go语言最大并发处理数,此处为 20 , “NUM_CPU”代表当前计算机的CPU总内核数,此处为 4,“NUM_GOROUTINE”代表当前时刻的并发程序的数量,此处为 2。
与并发处理有关的标记
标记名称 | 使用示例 | 说明 |
---|---|---|
-parallel | -parallel 4 | 功能:设置可并发执行的功能测试函数的最大数量 默认值:调用runtime.GOMAXPROCS(0)后的结果,即Go语言最大并发处理数量 先决条件:功能测试函数需要在开始处调用结构体testing.T类型的参数值的Parallel方法 生效的测试:功能测试 |
-cpu | -cpu 1,2,4 | 功能:根据标记的值,迭代的设置Go语言并发处理最大数并执行全部功能测试或全部基准测试。 迭代的次数与标记值中的整数个数一致 默认值:“”,即空字符串 先决条件:无 生效的测试:功能测试和基准测试 |
注意: -cpu 和 -parallel 标记的作用域都是代码包,它们只能用于控制某一个代码包内的测试的流程。如果使用 go test 命令启动了多个代码包的测试,那么每个代码包中的功能测试永远是可并发执行的,而基准测试永远是串行执行的。如果把针对某一个代码包的所有测试的运行过程看成一个整体的话,若在执行 go test 命令时加入了 -bench 标记,则针对各个代码包的测试运行过程会被串行地执行,否则它们将被并发地执行。但无论如何,打印测试记录和结果信息的动作是严格按照 go test 命令后面的代码包从左往右的顺序执行。
本篇讲解了Go语言程序测试的功能测试和基准测试,下篇继续讲解有关Go语言程序测试的有关知识。
最后附上国内的Go语言社区(每篇更新一个)
Golangtc.com: 该社区是众多的Go语言中文社区中比较活跃的一个。我们可以从中获知很多Go语言方面的信息。网址:http://www.golangtc.com