一、 Go test工具

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。

类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档

go test 命令会遍历所有的 *_test.go 文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

二、 测试函数

2.1 测试函数的格式

  1. 每个测试必须导入testing包

    1. func TestName(t *testing.T){
    2. // ...
    3. }
  2. 测试函数的名字必须以Test开头,后缀名必须以大写字母开头 ```go func TestAdd(t testing.T){ … } func TestSum(t testing.T){ … } func TestLog(t *testing.T){ … }

  1. 3. 参数t用于报告测试失败和附加的日志信息。testing.T 的拥有方法如下:
  2. ```go
  3. func (c *T) Error(args ...interface{})
  4. func (c *T) Errorf(format string, args ...interface{})
  5. func (c *T) Fail()
  6. func (c *T) FailNow()
  7. func (c *T) Failed() bool
  8. func (c *T) Fatal(args ...interface{})
  9. func (c *T) Fatalf(format string, args ...interface{})
  10. func (c *T) Log(args ...interface{})
  11. func (c *T) Logf(format string, args ...interface{})
  12. func (c *T) Name() string
  13. func (t *T) Parallel()
  14. func (t *T) Run(name string, f func(t *T)) bool
  15. func (c *T) Skip(args ...interface{})
  16. func (c *T) SkipNow()
  17. func (c *T) Skipf(format string, args ...interface{})
  18. func (c *T) Skipped() bool

2.2 测试函数示例

单元组件可以是函数、结构、方法和最终用户可能依赖的任何东西。
单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较!

  1. 定义一个 split 的包,包中定义一个 split 的函数,具体实现如下: ```go // split/split.go

package split

import “strings”

// split package with a single split function.

// Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep)

  1. for i > -1 {
  2. result = append(result, s[:i])
  3. s = s[i+1:]
  4. i = strings.Index(s, sep)
  5. }
  6. result = append(result, s)
  7. return

}

  1. 2. 在当前目录下,我们创建一个 **split_test.go **的测试文件,并定义一个测试函数如下:
  2. ```go
  3. // split/split_test.go
  4. package split
  5. import (
  6. "reflect"
  7. "testing"
  8. )
  9. func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
  10. got := Split("a:b:c", ":") // 程序输出的结果
  11. want := []string{"a", "b", "c"} // 期望的结果
  12. if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
  13. t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
  14. }
  15. }
  1. 此时split这个包中的文件如下:

image.png

  1. split包路径下,执行go test命令,可以看到输出结果如下:

image.png

  1. 一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go中添加如下测试函数: ```go func TestMoreSplit(t *testing.T) { got := Split(“abcd”, “bc”) want := []string{“a”, “d”} if !reflect.DeepEqual(want, got) {
    1. t.Errorf("excepted:%v, got:%v", want, got)
    } }
  1. 6. 再次运行go test命令,输出结果如下:
  2. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2639475/1626164012987-4f9cf90f-9250-4e0a-aa71-9f297e239d29.png#clientId=u422b648e-cadb-4&from=paste&height=190&id=u554786d2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=190&originWidth=705&originalType=binary&ratio=1&size=13480&status=done&style=none&taskId=ufe13d7dc-97ef-42db-8dce-fd2f3de3253&width=705)
  3. 7. go test -v来查看一下测试的函数名称和运行时间
  4. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2639475/1626164193992-e53e7010-a090-4ac9-bcef-662def1ebb3e.png#clientId=u422b648e-cadb-4&from=paste&height=245&id=u9e82e227&margin=%5Bobject%20Object%5D&name=image.png&originHeight=245&originWidth=486&originalType=binary&ratio=1&size=13601&status=done&style=none&taskId=u9f858cae-759b-414e-9627-af6bd7d474c&width=486)
  5. 8. 可以清楚看到上述测试没有成功,可以使用 go test -run , 它对应一个正则表达式,只有函数名匹配上的测试函数才会被 go test 命令执行. go test -v -run="More"
  6. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2639475/1626164372360-126304b9-0ebc-4230-8a9e-02cbc51c40a3.png#clientId=u422b648e-cadb-4&from=paste&height=172&id=u0447f302&margin=%5Bobject%20Object%5D&name=image.png&originHeight=172&originWidth=467&originalType=binary&ratio=1&size=10401&status=done&style=none&taskId=u23295e1d-19ef-46fc-8fb6-00e4710c54f&width=467)
  7. 9. 解决程序中出现的问题,最初的split 函数没有考虑到sep为多个字符的情况,修复这个bug
  8. ```go
  9. package split
  10. import "strings"
  11. // split package with a single split function.
  12. // Split slices s into all substrings separated by sep and
  13. // returns a slice of the substrings between those separators.
  14. func Split(s, sep string) (result []string) {
  15. i := strings.Index(s, sep)
  16. for i > -1 {
  17. result = append(result, s[:i])
  18. s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
  19. i = strings.Index(s, sep)
  20. }
  21. result = append(result, s)
  22. return
  23. }
  1. 再测试一次

image.png

2.3 测试组

我们现在还想要测试一下split函数对中文字符串的支持,这个时候我们可以再编写一个TestChineseSplit测试函数,但是我们也可以使用如下更友好的一种方式来添加更多的测试用例。

  1. func TestSplit(t *testing.T) {
  2. // 定义一个测试用例类型
  3. type test struct {
  4. input string
  5. sep string
  6. want []string
  7. }
  8. // 定义一个存储测试用例的切片
  9. tests := []test{
  10. {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
  11. {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
  12. {input: "abcd", sep: "bc", want: []string{"a", "d"}},
  13. {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
  14. }
  15. // 遍历切片,逐一执行测试用例
  16. for _, tc := range tests {
  17. got := Split(tc.input, tc.sep)
  18. if !reflect.DeepEqual(got, tc.want) {
  19. t.Errorf("excepted:%v, got:%v", tc.want, got)
  20. }
  21. }
  22. }

我们通过上面的代码把多个测试用例合到一起,再次执行go test命令。
image.png

前面多了一个空格,把测试用例部分加一个 %#v 来格式化

  1. func TestSplit(t *testing.T) {
  2. ...
  3. for _, tc := range tests {
  4. got := Split(tc.input, tc.sep)
  5. if !reflect.DeepEqual(got, tc.want) {
  6. t.Errorf("excepted:%#v, got:%#v", tc.want, got)
  7. }
  8. }
  9. }

这样提示就更明显了
image.png

2.4 子测试

测试用例比较多的时候,无法一眼看到哪个测试用例失败了,可以采取如下方法:

  1. func TestSplit(t *testing.T) {
  2. type test struct { // 定义test结构体
  3. input string
  4. sep string
  5. want []string
  6. }
  7. tests := map[string]test{ // 测试用例使用map存储
  8. "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
  9. "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
  10. "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
  11. "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
  12. }
  13. for name, tc := range tests {
  14. got := Split(tc.input, tc.sep)
  15. if !reflect.DeepEqual(got, tc.want) {
  16. t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got) // 将测试用例的name格式化输出
  17. }
  18. }
  19. }

上面的做法是能够解决问题的。同时Go1.7+中新增了子测试,我们可以按照如下方式使用t.Run执行子测试:

  1. func TestSplit(t *testing.T) {
  2. type test struct { // 定义test结构体
  3. input string
  4. sep string
  5. want []string
  6. }
  7. tests := map[string]test{ // 测试用例使用map存储
  8. "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
  9. "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
  10. "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
  11. "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
  12. }
  13. for name, tc := range tests {
  14. t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
  15. got := Split(tc.input, tc.sep)
  16. if !reflect.DeepEqual(got, tc.want) {
  17. t.Errorf("excepted:%#v, got:%#v", tc.want, got)
  18. }
  19. })
  20. }
  21. }

此时我们再执行go test命令就能够看到更清晰的输出内容了:
image.png
把测试用例修改回来:

  1. tests := map[string]test{ // 测试用例使用map存储
  2. "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
  3. "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
  4. "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
  5. "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
  6. }

image.png
我们都知道可以通过 -run=RegExp 来指定运行的测试用例,还可以通过/来指定要运行的子测试用例,
例如:go test -v -run=Split/simple 只会运行simple对应的子测试用例。

2.5 测试覆盖率

测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
Go提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:
image.png
从上面的结果可以看到我们的测试用例覆盖了100%的代码。
Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:
go test -cover -coverprofile=c.out

(这块用不了)image.png

上图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

三 . 基准测试

3.1 基准测试函数格式

基准测试基本格式:

  1. func BenchmarkName(b *testing.B){
  2. // ...
  3. }

基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B拥有的方法如下:

  1. func (c *B) Error(args ...interface{})
  2. func (c *B) Errorf(format string, args ...interface{})
  3. func (c *B) Fail()
  4. func (c *B) FailNow()
  5. func (c *B) Failed() bool
  6. func (c *B) Fatal(args ...interface{})
  7. func (c *B) Fatalf(format string, args ...interface{})
  8. func (c *B) Log(args ...interface{})
  9. func (c *B) Logf(format string, args ...interface{})
  10. func (c *B) Name() string
  11. func (b *B) ReportAllocs()
  12. func (b *B) ResetTimer()
  13. func (b *B) Run(name string, f func(b *B)) bool
  14. func (b *B) RunParallel(body func(*PB))
  15. func (b *B) SetBytes(n int64)
  16. func (b *B) SetParallelism(p int)
  17. func (c *B) Skip(args ...interface{})
  18. func (c *B) SkipNow()
  19. func (c *B) Skipf(format string, args ...interface{})
  20. func (c *B) Skipped() bool
  21. func (b *B) StartTimer()
  22. func (b *B) StopTimer()

3.2 基准测试示例

我们为split包中的Split函数编写基准测试如下:

  1. func BenchmarkSplit(b *testing.B) {
  2. for i := 0; i < b.N; i++ {
  3. Split("沙河有沙又有河", "沙")
  4. }
  5. }

基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:go test -bench=Split
image.png
其中 BenchmarkSplit-6 表示对Split函数进行基准测试,数字6表示GOMAXPROCS的值,这个对于并发基准测试很重要。3836172371.3ns/op表示每次调用Split函数耗时371.3 ns,这个结果是10000000次调用的平均值。

我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。go test -bench=Split -benchmem
image.png
其中,112B/op 表示每次操作内存分配了112字节,3则表示每次操作进行了3次内存分配,将我们的split函数优化如下:

  1. func Split(s, sep string) (result []string) {
  2. result = make([]string, 0, strings.Count(s, sep)+1)
  3. i := strings.Index(s, sep)
  4. for i > -1 {
  5. result = append(result, s[:i])
  6. s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
  7. i = strings.Index(s, sep)
  8. }
  9. result = append(result, s)
  10. return
  11. }

这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升:
image.png
这个使用make函数提前分配内存的改动,减少了2/3的内存分配次数,并且减少了一半的内存分配。

3.3 性能比较函数

这个函数有点问题!
与示例不一样(性能比较函数
测试结果如下:
image.png

3.4 重置时间

b.ResetTimer 之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。例如:

  1. func BenchmarkSplit(b *testing.B) {
  2. time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
  3. b.ResetTimer() // 重置计时器
  4. for i := 0; i < b.N; i++ {
  5. Split("沙河有沙又有河", "沙")
  6. }
  7. }

clipboardErrorCopied

3.5 并行测试

func (b B) RunParallel(body func(PB)) 会以并行的方式执行给定的基准测试。


RunParallel会创建出多个goroutine,并将b.N分配给这些goroutine执行, 其中goroutine数量的默认值为GOMAXPROCS。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel之前调用SetParallelism 。RunParallel通常会与-cpu标志一同使用。

go test -bench=. -cpu 1
image.png

四 . Setup与TearDown

测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。

4.1TestMain

通过在 *_test.go 文件中定义TestMain 函数来可以在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)操作。


如果测试文件包含函数:func TestMain(m *testing.M)那么生成的测试会先调用 TestMain(m),然后再运行具体测试。TestMain运行在主goroutine中, 可以在调用 m.Run前后做任何设置(setup)和拆卸(teardown)。退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit。

一个使用TestMain来设置Setup和TearDown的示例如下:

  1. func TestMain(m *testing.M) {
  2. fmt.Println("write setup code here...") // 测试之前的做一些设置
  3. // 如果 TestMain 使用了 flags,这里应该加上flag.Parse()
  4. retCode := m.Run() // 执行测试
  5. fmt.Println("write teardown code here...") // 测试之后做一些拆卸工作
  6. os.Exit(retCode) // 退出测试
  7. }

需要注意的是:在调用TestMain时, flag.Parse并没有被调用。所以如果TestMain 依赖于command-line标志 (包括 testing 包的标记), 则应该显示的调用flag.Parse。

4.2子测试的Setup与Teardown

有时候我们可能需要为每个测试集设置Setup与Teardown,也有可能需要为每个子测试设置Setup与Teardown。下面我们定义两个函数工具函数如下:

  1. // 测试集的Setup与Teardown
  2. func setupTestCase(t *testing.T) func(t *testing.T) {
  3. t.Log("如有需要在此执行:测试之前的setup")
  4. return func(t *testing.T) {
  5. t.Log("如有需要在此执行:测试之后的teardown")
  6. }
  7. }
  8. // 子测试的Setup与Teardown
  9. func setupSubTest(t *testing.T) func(t *testing.T) {
  10. t.Log("如有需要在此执行:子测试之前的setup")
  11. return func(t *testing.T) {
  12. t.Log("如有需要在此执行:子测试之后的teardown")
  13. }
  14. }

使用方式如下:

  1. func TestSplit(t *testing.T) {
  2. type test struct { // 定义test结构体
  3. input string
  4. sep string
  5. want []string
  6. }
  7. tests := map[string]test{ // 测试用例使用map存储
  8. "simple": {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
  9. "wrong sep": {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
  10. "more sep": {input: "abcd", sep: "bc", want: []string{"a", "d"}},
  11. "leading sep": {input: "沙河有沙又有河", sep: "沙", want: []string{"", "河有", "又有河"}},
  12. }
  13. teardownTestCase := setupTestCase(t) // 测试之前执行setup操作
  14. defer teardownTestCase(t) // 测试之后执行testdoen操作
  15. for name, tc := range tests {
  16. t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
  17. teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作
  18. defer teardownSubTest(t) // 测试之后执行testdoen操作
  19. got := Split(tc.input, tc.sep)
  20. if !reflect.DeepEqual(got, tc.want) {
  21. t.Errorf("excepted:%#v, got:%#v", tc.want, got)
  22. }
  23. })
  24. }
  25. }

clipboardErrorCopied

五 . 示例函数

5.1示例函数的格式

被go test特殊对待的第三种函数就是示例函数,它们的函数名以Example为前缀。它们既没有参数也没有返回值。标准格式如下:

  1. func ExampleName() {
  2. // ...
  3. }

5.2示例函数示例

  1. func ExampleSplit() {
  2. fmt.Println(split.Split("a:b:c", ":"))
  3. fmt.Println(split.Split("沙河有沙又有河", "沙"))
  4. // Output:
  5. // [a b c]
  6. // [ 河有 又有河]
  7. }

六、练习题

  1. 编写一个回文检测函数,并为其编写单元测试和基准测试,根据测试的结果逐步对其进行优化。(回文:一个字符串正序和逆序一样,如“Madam,I’mAdam”、“油灯少灯油”等。)