Go语言学习17-功能测试

引言

Go语言中提供了 go test 命令,它不仅仅可以对代码包进行测试,还可以对个别源码文件进行测试,只要存在针对这些测试的测试源码文件。除此之外,Go语言还在标准库中提供了一个专门用于测试的代码包 testing,它提供了编写测试源码文件所需的一切。

主要内容

1. 编写功能测试函数

测试源码文件总应该与被它测试的源码文件处于同一代码包内。在编写测试源码文件的时候,总是会用到标准库代码包 testing 中的 APItesting 包为Go语言的代码包提供了自动化测试支持。它的目标是与 go test 命令协同使用,以自动执行目标代码包中的任何测试函数。

在测试源码文件中,针对其他源码文件中的程序实体的功能测试程序总是以函数为单位的。被用于测试程序实体功能的函数的名称和签名形如:

1
func TestXxx(t *testing.T)

其中,Xxx 应该是大写字母开头的若干字母或数字的组合,通常情况下会将 Xxx 替换成被测试的程序实体的名称。可以利用 *testing.T 类型的参数 t 上的一些方法对功能测试的过程进行记录和控制。使用 t 的值上的方法记录的信息会在测试结束之后(不论成败),一并打印到标准输出上。

2. 常规记录

参数 t 上的 LogLogf 方法一般用于记录一些常规信息,以展示测试程序的运行过程以及被测试程序实体的实时状态。调用语句如下:

1
2
t.Log("Tomorrow is a ", "good ", "day ") // 类似于fmt.Println 
t.Logf("Tomorrow is a %s", " good day ") // 类似于fmt.Printf

使用 go test –v 命令,两者都会打印如下信息:

1
2
xxx_test.go:10: Tomorrow is a good day
xxx_test.go:11: Tomorrow is a good day
打印信息 解释
xxx_test.go 调用语句所在的测试源码文件的 名称
10 调用语句出现的 行号

3. 错误记录

参数 t 上的 ErrorErrorf 方法被用于记录错误信息。当被测试的程序实体的状态不正确的时候,使用 t.Errort.Errorf 方法,及时对当前的错误状态进行记录。例如:

1
2
3
4
actLen := len(s)
if actLen != expLen {
t.Errorf("Error: The length of slice should be %d but %d.\n", expLen, actLen)
}

调用 t.Error 方法相当于先后对 t.Logt.Fail 方法进行调用,而调用 t.Errorf 方法则相当于与先后对 t.Logft.Fail 方法进行调用。

4. 致命错误记录

参数 t 上的 FatalFatalf 方法被用于记录致命的程序实体的状态错误。所谓致命错误是指使得测试无法继续进行的错误。例如:

1
2
3
if listener == nil {
t.Fatalf("Listener startup failing! (addr=%s)!\n", serverAddr)
}

调用 t.Fatal 方法相当于先后对 t.Logt.FailNow 方法进行调用,而调用 t.Fatalf 方法则相当于先后对 t.Logft.FailNow 方法进行调用。

5. 失败标记

如果需要标记当前测试函数中的测试是失败的,那么就需要用到 t.Fail 方法。对 t.Fail 方法的调用不会终止当前测试函数的执行。但是,此函数的测试结果已经被标记为失败了。

6. 立即失败标记

方法 t.FailNowt.Fail 不同的地方是,它在被调用时会立即终止当前测试函数的执行。这会使得当前的测试运行程序转而去执行其他的测试函数。

注意: 只能在运行测试函数的 Goroutine 中调用 t.FailNow 方法,而不能在测试代码创建出的 Goroutine 中调用它。不过,在其他的 Goroutine 中调用 t.FailNow 方法也不会造成什么错误,只是它不会产生任何效果而已。

7. 失败判断

在调用 t.Failed 方法之后,会获得一个 bool 类型的结果值,它代表了当前的测试函数中的测试是否已被标记为失败。

8. 忽略测试

调用 t.SkipNow 方法目的是标记当前测试函数为已经被忽略,并且立即终止该函数的执行,当前的测试运行程序会转而去执行其他测试函数。与 t.FailNow 方法相同,t.SkipNow 方法也只能在运行测试函数的 Goroutine 中被调用。

调用 t.Skip 方法相当于先后对 t.Logt.SkipNow 方法进行调用,而调用 t.Skipf 方法则相当于先后对 t.Logft.SkipNow 方法进行调用。

方法 t.Skipped 的结果值会告知当前的测试是否已被忽略。

9. 并行运行

方法 t.Parallel 的调用会使当前的测试函数被标记为可并行运行的。这会使测试运行程序可以并发地执行它以及其他可并行运行的测试函数。

10. 功能测试的运行

使用 goc2p 项目的代码包 cnet/ctcppkgtool 为例,如下是下载地址:

https://github.com/hyper-carrot/go_command_tutorial

该代码包中仅包含一个名为 tcp_test.go 的测试源码文件。该测试源码文件包含了两个测试函数。一个是名为 TestPrimeFuncs 的功能测试函数,一个是名为 BenchmarkPrimeFuncs 的基准测试函数。

使用 go test 命令运行 cnet/ctcp 包中的测试结果如下截图:

如果只想运行代码包中部分测试的话,有两种方式可以选择:

  • 第一种是 go test 命令后面以测试源码文件及其测试的源码文件为参数,而不是代码包。例如:

    1
    go test envir_test.go envir.go
  • 第二种是使用标记 -run 。**-run** 标记的值应该为一个正则表达式。名称与此正则表达式匹配的功能测试函数,才会在当次的测试运行过程中被执行。运行截图如下:

    该代码包的测试源码文件 tcp_test.go 中的功能测试函数 TestPrimeFuncs 会被执行。但当正则表达式改为 Prima 后,由于没有 cnet/ctcp 包并没有名称与之匹配的功能测试函数。运行截图如下:

在Go语言中,可以通过方法 t.Logt.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毫秒,并运行测试。如下:

1
2
3
4
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 ,那么就可以根据具体情况,去剪裁测试代码从而缩短测试运行时间了。可以在一个功能测试函数中写一段类似的代码:

1
2
3
4
5
6
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()**。

结语

本篇讲解了Go语言程序测试的 功能测试,下篇讲解Go语言程序测试的 基准测试,尽情期待。

最后附上国内的Go语言社区

Golangtc.com: 该社区是众多的Go语言中文社区中比较活跃的一个。我们可以从中获知很多Go语言方面的信息。网址:http://www.golangtc.com