Go:单元测试

时间:Feb. 19, 2020 分类:

目录:

go test工具

Go中测试依赖go test命令,测试的代码就是Go代码,在包内以_test.go结尾的文件就是测试代码,不会被编译到可执行文件中

*_test.go有三种类型的函数

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

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

测试函数

测试函数的格式

测试函数需要导入testing包

func TestName(t *testing.T) {
    ...
}

参数t用于报告测试失败和附加的日志信息,拥有方法有

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool

测试函数示例

先定义一个split包和一个Split函数

split/split.go

package split

import "strings"

func Split(str string, sep string) (ret []string) {
    index := strings.Index(str, sep)
    for index >= 0 {
        ret = append(ret, str[:index])
        str = str[index+1:]
        index = strings.Index(str, sep)
    }
    ret = append(ret, str)
    return
}

split/split_test.go

package split

import (
    "reflect"
    "testing"
)

// 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
func TestSplit(t *testing.T) {
    got := Split("a:b:c", ":")         // 程序输出的结果
    want := []string{"a", "b", "c"}    // 期望的结果
    // 因为slice不能比较直接,借助反射包中的方法比较
    if !reflect.DeepEqual(want, got) {              
        // 测试失败输出错误提示
        t.Errorf("excepted:%v, got:%v", want, got) 
    }
}

进行测试

$ go test
PASS
ok      split   0.002s

如果测试失败可以将want修改,例如改为[]string{"a", "b", "cd"}

$ go test
--- FAIL: TestSplit (0.00s)
    split_test.go:15: excepted:[a b cd], got:[a b c]
FAIL
exit status 1
FAIL    split   0.002s

可以再增加一个测试

package split

import (
    "reflect"
    "testing"
)

// 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
func TestSplit(t *testing.T) {
    got := Split("a:b:c", ":")         // 程序输出的结果
    want := []string{"a", "b", "c"}    // 期望的结果
    // 因为slice不能比较直接,借助反射包中的方法比较
    if !reflect.DeepEqual(want, got) {              
        // 测试失败输出错误提示
        t.Errorf("excepted:%v, got:%v", want, got) 
    }
}

func TestMoreSplit(t *testing.T) {
    got := Split("a-==-b-==-c", "-==-")  // 程序输出的结果
    want := []string{"a", "b", "c"}    // 期望的结果
    // 因为slice不能比较直接,借助反射包中的方法比较
    if !reflect.DeepEqual(want, got) {              
        // 测试失败输出错误提示
        t.Errorf("excepted:%v, got:%v", want, got) 
    }
}

打印结果

$ go test
--- FAIL: TestMoreSplit (0.00s)
    split_test.go:25: excepted:[a b c], got:[a ==-b ==-c]
FAIL
exit status 1
FAIL    split   0.001s

使用-v参数,查看测试函数名称和运行时间

$ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
    split_test.go:25: excepted:[a b c], got:[a ==-b ==-c]
FAIL
exit status 1
FAIL    split   0.002s

这样这个分隔符如果是一个非1长度的字符串就失效了,修复的时候把1换成len(sep)就好了

测试组

测试组就是通过struct来构建一组测试参数和期望结果,通过循环的方式进行单元测试

package split

import (
    "reflect"
    "testing"
)


func TestGroupSplit(t *testing.T) {
    type test struct {
        str   string
        sep   string
        want  []string
    }
    tests := []test{
        {str: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        {str: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        {str: "abcd", sep: "bc", want: []string{"a", "d"}},
        {str: "一二一一二三四", sep: "二", want: []string{"一", "一一", "三四"}},
    }
    // 遍历切片,逐一执行测试用例
    for _, tc := range tests {
        got := Split(tc.str, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("excepted:%v, got:%v", tc.want, got)
        }
    }
}

子测试

因为在测试中可能不能正确的发现哪个用例失败了,将测试组的splice改为map

=== RUN   TestGroupSplit
=== RUN   TestGroupSplit/simple
=== RUN   TestGroupSplit/wrong_sep
=== RUN   TestGroupSplit/more_sep
=== RUN   TestGroupSplit/leading_sep
--- PASS: TestGroupSplit (0.00s)
    --- PASS: TestGroupSplit/simple (0.00s)
    --- PASS: TestGroupSplit/wrong_sep (0.00s)
    --- PASS: TestGroupSplit/more_sep (0.00s)
    --- PASS: TestGroupSplit/leading_sep (0.00s)
PASS
ok      split   0.002s

执行结果

=== RUN   TestGroupSplit
=== RUN   TestGroupSplit/simple
=== RUN   TestGroupSplit/wrong_sep
=== RUN   TestGroupSplit/more_sep
=== RUN   TestGroupSplit/leading_sep
--- PASS: TestGroupSplit (0.00s)
    --- PASS: TestGroupSplit/simple (0.00s)
    --- PASS: TestGroupSplit/wrong_sep (0.00s)
    --- PASS: TestGroupSplit/more_sep (0.00s)
    --- PASS: TestGroupSplit/leading_sep (0.00s)
PASS
ok      split   0.002s

测试覆盖率

$ go test -cover
PASS
coverage: 100.0% of statements
ok      split   0.002s

可以将详细的情况输出到文件进而转化为html

$ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok      split   0.002s
$ cat c.out 
mode: set
split/split.go:5.51,7.20 2 1
split/split.go:12.5,13.11 2 1
split/split.go:7.20,11.6 3 1
$ go tool cover -html=c.out
HTML output written to /tmp/cover955594976/coverage.html

就是这个样子

基准测试

基准测试时在一定的工作负载下检测程序性能的一种方法

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

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

testing.B有以下方法

func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
func (b *B) StopTimer()

基准测试示例

示例基准测试代码

package split

import (
    "reflect"
    "testing"
)

func BenchmarkSplit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Split("一二一一二三四", "二")
    }
}

执行测试命令,需要增加-bench参数

$ go test -bench=Split
goos: linux
goarch: amd64
pkg: split
BenchmarkSplit   5000000           348 ns/op
PASS
ok      split   2.079s

调用了5000000次,平均调用时长为348ns

可以获取到内存分配的统计数据

$ go test -bench=Split -benchmem
goos: linux
goarch: amd64
pkg: split
BenchmarkSplit   5000000           346 ns/op         112 B/op          3 allocs/op
PASS
ok      split   2.079s

112B代表每次操作内存分配了112字节,3代表每次操作进行了3次内存分配

修改为提前申请好内存

package split

import "strings"

func Split(str string, sep string) (ret []string) {
    ret = make([]string, 0, strings.Count(str, sep)+1)
    index := strings.Index(str, sep)
    for index >= 0 {
        ret = append(ret, str[:index])
        str = str[index+len(sep):]
        index = strings.Index(str, sep)
    }
    ret = append(ret, str)
    return
}

再进行一次基准测试

$ go test -bench=Split -benchmem
goos: linux
goarch: amd64
pkg: split
BenchmarkSplit  10000000           215 ns/op          48 B/op          1 allocs/op
PASS
ok      split   2.370s

性能比较函数

之前的基准测试,只能测试给定操作的绝对值,但是对于绝多数性能问题,是发生在两个不同操作之间的相对耗时,比如一个同一个函数处理1000个元素和处理1万甚至100万的耗时差距是多少?

示例性能比较函数

func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }

测试一个斐波那契数列的函数

func Fib(n int) int {
    if n < 2 {
        return n
    }
    return Fib(n-1) + Fib(n-2)
}

基准测试函数

func benchmarkFib(b *testing.B, n int) {
    for i := 0; i < b.N; i++ {
        Fib(n)
    }
}

func BenchmarkFib1(b *testing.B)  { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }

测试结果

$ go test -bench=.
goos: linux
goarch: amd64
pkg: split
BenchmarkFib1   1000000000           2.92 ns/op
BenchmarkFib2   200000000            8.53 ns/op
BenchmarkFib3   100000000           13.1 ns/op
BenchmarkFib10   3000000           557 ns/op
BenchmarkFib20     30000         59637 ns/op
BenchmarkFib40         1    1155978074 ns/op
PASS
ok      split   13.960s

看到40的时候只执行了一次,可以调高基准测试时间

$ go test -bench=Fib40 -benchtime=20s
goos: linux
goarch: amd64
pkg: split
BenchmarkFib40        30     954169460 ns/op
PASS
ok      split   29.514s

性能测试的错误问题

b.N是不能作为大小进行输入

// 错误示范1
func BenchmarkFibWrong(b *testing.B) {
    for n := 0; n < b.N; n++ {
        Fib(n)
    }
}

// 错误示范2
func BenchmarkFibWrong2(b *testing.B) {
    Fib(b.N)
}

重置时间

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

func BenchmarkSplit(b *testing.B) {
    time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
    b.ResetTimer()              // 重置计时器
    for i := 0; i < b.N; i++ {
        Split("一二一一二三四", "二")
    }
}

并行测试

func (b *B) RunParallel(body func(*PB))

RunParallel会以并行的方式执行给定的基准测试

RunParallel会创建出多个goroutine,并将b.N分配给goroutine执行,其中goroutine数量为GOMAXPROCS,如果想要不受CPU影响,可以在RunParallel之前调用SetParallelism

func BenchmarkSplitParallel(b *testing.B) {
    // b.SetParallelism(1) // 设置使用的CPU数
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Split("一二一一二三四", "二")
        }
    })
}

Setup与TearDown

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

TestMain

如果测试文件包含函数func TestMain(m *testing.M)那么生成的测试会先调用TestMain(m),然后再运行具体测试。

TestMain运行在主goroutine中, 可以在调用m.Run前后做任何设置(setup)和拆卸(teardown)。退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit

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

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

需要注意的是

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

子测试的Setup与Teardown

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

// 测试集的Setup与Teardown
func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("如有需要在此执行:测试之前的setup")
    return func(t *testing.T) {
        t.Log("如有需要在此执行:测试之后的teardown")
    }
}

// 子测试的Setup与Teardown
func setupSubTest(t *testing.T) func(t *testing.T) {
    t.Log("如有需要在此执行:子测试之前的setup")
    return func(t *testing.T) {
        t.Log("如有需要在此执行:子测试之后的teardown")
    }
}

使用方式

func TestSplit(t *testing.T) {
    type test struct { // 定义test结构体
        input string
        sep   string
        want  []string
    }
    tests := map[string]test{ // 测试用例使用map存储
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "一二一一二三四", sep: "二", want: []string{"一", "一一", "三四"}},
    }
    teardownTestCase := setupTestCase(t) // 测试之前执行setup操作
    defer teardownTestCase(t)            // 测试之后执行testdoen操作

    for name, tc := range tests {
        t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
            teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作
            defer teardownSubTest(t)           // 测试之后执行testdoen操作
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                t.Errorf("excepted:%#v, got:%#v", tc.want, got)
            }
        })
    }
}

测试结果如下:

$ go test -v
=== RUN   TestSplit
=== RUN   TestSplit/simple
=== RUN   TestSplit/wrong_sep
=== RUN   TestSplit/more_sep
=== RUN   TestSplit/leading_sep
--- PASS: TestSplit (0.00s)
    split_test.go:71: 如有需要在此执行:测试之前的setup
    --- PASS: TestSplit/simple (0.00s)
        split_test.go:79: 如有需要在此执行:子测试之前的setup
        split_test.go:81: 如有需要在此执行:子测试之后的teardown
    --- PASS: TestSplit/wrong_sep (0.00s)
        split_test.go:79: 如有需要在此执行:子测试之前的setup
        split_test.go:81: 如有需要在此执行:子测试之后的teardown
    --- PASS: TestSplit/more_sep (0.00s)
        split_test.go:79: 如有需要在此执行:子测试之前的setup
        split_test.go:81: 如有需要在此执行:子测试之后的teardown
    --- PASS: TestSplit/leading_sep (0.00s)
        split_test.go:79: 如有需要在此执行:子测试之前的setup
        split_test.go:81: 如有需要在此执行:子测试之后的teardown
    split_test.go:73: 如有需要在此执行:测试之后的teardown
=== RUN   ExampleSplit
--- PASS: ExampleSplit (0.00s)
PASS
ok      split       0.006s

示例函数

示例函数的主要用途是为Split函数编写一个示例

func ExampleSplit() {
    fmt.Println(split.Split("a:b:c", ":"))
    fmt.Println(split.Split("一二一一二三四", "二"))
    // Output:
    // [a b c]
    // [一 一一 三四]
}

用途有

  1. 作为文档使用
  2. 包含了// Output:可以通过go test进行测试
  3. 可以在golang.org的godoc文档服务器上使用Go Playground运行示例代码