Go基础编程 - 13 -单元测试

上一篇:流程控制

下一篇:泛型


不测试的开发不是好程序员。

1. go test 工具

Go语言的测试依赖 go test 命令。在包目录中,所有以 _test.go 为后缀名的源代码文件都是go test测试的一部分,不会被 go build 编译。

文件名必须以 _test.go 结尾(例如:xx_tes.go)。

在 _test.go 文件中有三种类型的函数:单元测试函数、基准测试函数、示例函数

类型格式作用示例
测试函数函数名前缀为Test测试程序的逻辑是否正确TestFunc()
基准函数函数名前缀为Benchmark测试程序的性能是否达到预期BenchmarkFunc()
示例函数函数名前缀为Example文档示例ExampleFunc()

go test 参数:

  • 通过 go help test 查看 go test 使用说明。
  • 命令格式:go test [-c] [-i] [build flags] [packages] [flags for test binary]

常用参数说明:

-c:编译 go test 成可执行的二进制文件,但不运行测试。
-i:安装测试包依赖的 package,但不运行测试。
测试函数
go test -v:输出全部单元测试用例,默认(不加)只输出失败的测试用例。
go test -run="regexp":只运行指定的单元测试用例,regexp 为正则表达式。
go test -cover:测试覆盖率。显示包函数被单元测试用例覆盖的百分比。
go test -cover -coverprofile="xxx.out":编写一个基准测试覆盖率概要到当前文件夹下的xxx.out文件中。
go tool cover -html="xxx.out":使用 cover 工具处理生成的覆盖率记录文件,该命令会打开本地的浏览器窗口生成一个HTML报告。
基准测试
go test -bench="regexp":运行指定的基准测试用例,regexp 为正则表达式。基准测试默认不执行,需要加上 -bench 选项
go test -bench="regexp" -benchmem :运行基准测试用例时输出内存情况。
go test -bench="regexp" -benchtime=10s:设置运行每个基准测试持续的时间(默认1s)

官方文档:

  -test.bench regexp
        run only benchmarks matching regexp
  -test.benchmem
        print memory allocations for benchmarks
  -test.benchtime d
        run each benchmark for duration d (default 1s)
  -test.blockprofile file
        write a goroutine blocking profile to file
  -test.blockprofilerate rate
        set blocking profile rate (see runtime.SetBlockProfileRate) (default 1)
  -test.count n
        run tests and benchmarks n times (default 1)
  -test.coverprofile file
        write a coverage profile to file
  -test.cpu list
        comma-separated list of cpu counts to run each test with
  -test.cpuprofile file
        write a cpu profile to file
  -test.failfast
        do not start new tests after the first test failure
  -test.fuzz regexp
        run the fuzz test matching regexp
  -test.fuzzcachedir string
        directory where interesting fuzzing inputs are stored (for use only by cmd/go)
  -test.fuzzminimizetime value
        time to spend minimizing a value after finding a failing input (default 1m0s)
  -test.fuzztime value
        time to spend fuzzing; default is to run indefinitely
  -test.fuzzworker
        coordinate with the parent process to fuzz random values (for use only by cmd/go)
  -test.list regexp
        list tests, examples, and benchmarks matching regexp then exit
  -test.memprofile file
        write an allocation profile to file
  -test.memprofilerate rate
        set memory allocation profiling rate (see runtime.MemProfileRate)
  -test.mutexprofile string
        write a mutex contention profile to the named file after execution
  -test.mutexprofilefraction int
        if >= 0, calls runtime.SetMutexProfileFraction() (default 1)
  -test.outputdir dir
        write profiles to dir
  -test.paniconexit0
        panic on call to os.Exit(0)
  -test.parallel n
        run at most n tests in parallel (default 4)
  -test.run regexp
        run only tests and examples matching regexp
  -test.short
        run smaller test suite to save time
  -test.shuffle string
        randomize the execution order of tests and benchmarks (default "off")
  -test.testlogfile file
        write test action log to file (for use only by cmd/go)
  -test.timeout d
        panic test binary after duration d (default 0, timeout disabled)
  -test.trace file
        write an execution trace to file
  -test.v
        verbose: print additional output

2. 测试函数

单元测试是一些利用各种方法测试单元组件的程序,它将结果与预期输出进行比较,以验证程序的正确性。

2.1. 格式及参数

// 测试函数:方法名必须以 Test 开头,Test后首字母必须大写(如:Name),参数必须为 t *testing.T
func TestName(t *testing.T) {
}

testing.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) Helper()
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

2.2. 测试函数示例

结下来我们定义一个split包,包中定义了一个Split函数,如下:

// split/split.go
package split

import "strings"

func Split(s, sep string) []string {
    var res []string

    i := strings.Index(s, sep)
    for i > -1 {
        res = append(res, s[:i])
        s = s[i+1:]
        i = strings.Index(s, sep)
    }
    res = append(res, s)
    return res
}

然后我们在split包中编写一个单元测试,如下:

// 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"} // 期望结果
    if !reflect.DeepEqual(got, want) {  // 借助reflect.DeepEqual方法比较结果
        t.Errorf("fail: want:%v, got:%v", want, got) // 失败,输出提示
    }
}

在 split 包路径下,执行 go test 命令,如下:

PS F:/Learing/test/split> go test
PASS
ok      Learing/test/split      0.305s

若在 split_test.go 新增测试方法 TestSplit2,如下

func TestSplit2(t *testing.T) {
    got := Split("a||b||c", "||")  
    want := []string{"a", "b", "c"} 
    if !reflect.DeepEqual(got, want) {  
        t.Errorf("fail: want:%v, got:%v", want, got) 
    }
}

执行 go test 显示如下错误(因split不支持sep多个字符):

PS F:/Learing/test/split> go test   
--- FAIL: TestSplit2 (0.00s)
    split_test.go:20: fail: want:[a b c], got:[a |b |c]
FAIL
exit status 1
FAIL    Learing/test/split      0.468s

go test 只展示失败的测试用例,若查看所有测试函数的详细信息,执行 go test -v 命令,如下:

PS F:\Learning\test\split> go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestSplit2
    split_test.go:20: fail: want:[a b c], got:[a |b |c]
--- FAIL: TestSplit2 (0.00s)
FAIL
exit status 1
FAIL    Learing/test/split      0.241s

go test -run="regexp" 仅执行匹配到的测试用例,如下仅执行了 TestSplit2 的测试用例:

PS F:\Learning\test\split> go test -v -run="2"
=== RUN   TestSplit2
    split_test.go:12: fail: want:[a b c], got:[a |b |c]
--- FAIL: TestSplit2 (0.00s)
FAIL
exit status 1
FAIL    Learing/test/split      0.227s

2.3. 子测试

上面 split_test.go 中写了两个测试函数,一个是 TestSplit,一个是 TestSplit2,分别测试两个用例。我们使用更友好的方式,将多个测试用例放在一个测试组中;且在Go1.7+中新增了子测试,我们可以按照如下方式使用t.Run执行子测试。如下:

// split/split.go

func Split(s, sep string) []string {
	var res []string

	i := strings.Index(s, sep)
	for i > -1 {
		res = append(res, s[:i])
		s = s[i+len(sep):]  // 优化,使用len(sep)获取sep的长度,支持sep字符串
		i = strings.Index(s, sep)
	}
	res = append(res, s)
	return res
}
func TestSplit(t *testing.T) {
    // 定义一个测试用例类型
    type test struct {
        s    string
        sep  string
        want []string
    }

    // 定义一组测试用例(测试组)
    tests := map[string]test{
        "simple":        {s: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "multichar sep": {s: "a||b||c", sep: "||", want: []string{"a", "b", "c"}},
        "中文sep":         {s: "小明:18:男", sep: ":", want: []string{"小明", "18", "男"}},
    }

    // 遍历用例进行测试
    for name, tc := range tests {
        t.Run(name, func(t *testing.T) {
            if got := Split(tc.s, tc.sep); !reflect.DeepEqual(got, tc.want) {
                t.Errorf("fail: want:%#v, got:%#v", tc.want, got) //推荐使用%#v的格式化
            }
        })
    }
}

执行 go test -v 输出如下:

PS F:\Learning\test\split> go test -v
=== RUN   TestSplit
=== RUN   TestSplit/simple
=== RUN   TestSplit/multichar_sep
=== RUN   TestSplit/中文sep
--- PASS: TestSplit (0.00s)
    --- PASS: TestSplit/simple (0.00s)
    --- PASS: TestSplit/multichar_sep (0.00s)
    --- PASS: TestSplit/中文sep (0.00s)
PASS
ok      Learing/test/split      0.194s

子测试同样可以使用-run="regexp"进行筛选,但前面须加上函数名,如下:

PS F:\Learning\test\split> go test -v -run="Split/sep"
=== RUN   TestSplit
=== RUN   TestSplit/multichar_sep
=== RUN   TestSplit/中文sep
--- PASS: TestSplit (0.00s)
    --- PASS: TestSplit/multichar_sep (0.00s)
    --- PASS: TestSplit/中文sep (0.00s)
PASS
ok      Learing/test/split      0.227s

注意:子测试筛选必须使用函数名作为路径,上面示例中使用 Split/ 不可使用 TestSplit/

2.4. 测试覆盖率

测试覆盖率,是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句覆盖率,也就是在测试中至少被运行一次的代码占总代吗的比例。
Go语言使用go test -cover来查看测试覆盖率。
例如,我们在 split/split.go 文件中在增加两个方法:

// split/split.go
package split

import (
	"strings"
)

func Split(s, sep string) []string {
	var res []string

	i := strings.Index(s, sep)
	for i > -1 {
		res = append(res, s[:i])
		s = s[i+len(sep):]
		i = strings.Index(s, sep)
	}
	res = append(res, s)
	return res
}

// 优化Split函数,事先开辟好内存空间,减少append操作
func SplitHigh(s, sep string) (res []string) {

	res = make([]string, 0, strings.Count(s, sep)+1)
	i := strings.Index(s, sep)
	for i > -1 {
		res = append(res, s[:i])
		s = s[i+len(sep):]
		i = strings.Index(s, sep)
	}
	res = append(res, s)
	return
}

// Fib 计算第n个斐波那契数的函数
func Fib(n int) int {
	if n <= 0 {
		return 0
	} else if n <= 2 {
		return 1
	}
	return Fib(n-1) + Fib(n-2)
}

执行 go test -cover

PS F:\Learning\test\split> go test -cover
PASS
coverage: 38.1% of statements
ok      Learing/test/split      0.233s

Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。如下命令,在当前目录生成了一个split.out文件,其中包含测试覆盖率信息。

PS F:\Learning\test\split> go test -cover -coverprofile="split.out"
PASS
coverage: 38.1% of statements
ok      Learing/test/split      0.278s

go tool cover -html=split.out 使用 cover 工具处理生成的记录,打开本地浏览器窗口生成一个HTML报告。使用 go tool cover -help 查看命令详情。

3. 基准函数

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

3.1. 基准测试函数格式

// 必须以 Benchmark 为前缀,之后首字母必须大写(如:Name),参数必须为 *testing.B
func BenchmarkName(b *testing.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()

3.2. 基准测试示例

在 split_test.go 中为Split函数及SplitHigh函数编写基准测试:

func BenchmarkSplit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Split("a:b:c", ":")
    }
}

func BenchmarkSplitHigh(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SplitHigh("a:b:c", ":")
    }
}

基准函数不会默认执行,需增加 -bench 参数,执行 go test -bench=Split (-bench="." 执行当前包下所有基准函数),输出如下:

PS F:\Learning\test\split> go test -bench=Split
goos: windows
goarch: amd64
pkg: Learing/test/split
cpu: Intel(R) Core(TM) i5-4460  CPU @ 3.20GHz
BenchmarkSplit-4         4828096               248.6 ns/op
BenchmarkSplitHigh-4    10184638               112.9 ns/op
PASS
ok      Learing/test/split      2.997s

其中 BenchmarkSplit-4 中 4 表示 GOMAXPROCS 的值,对于并发基准测试很重要;4828096、248.6 ns/op 表示调用Split函数4828096次平均每次耗时为248.6ns。

还可以为基准测试添加 -benchmem 参数,来获得内存分配的统计数据。

PS F:\Learning\test\split> go test -bench=Split -benchmem
goos: windows
goarch: amd64
pkg: Learing/test/split
cpu: Intel(R) Core(TM) i5-4460  CPU @ 3.20GHz
BenchmarkSplit-4         4712768               247.2 ns/op           112 B/op          3 allocs/op
BenchmarkSplitHigh-4    10606215               114.6 ns/op            48 B/op          1 allocs/op
PASS
ok      Learing/test/split      2.958s

其中,112 B/op表示每次操作内存分配了112字节,3 allocs/op则表示每次操作进行了3次内存分配。
对比 SplitSplitHigh 函数,使用make函数提前分配内存的改动,减少了2/3的内存分配次数,并且减少了一半的内存分配,执行时间减少了一半。

3.3. 性能比较函数

上面的基准测试只能得到给定操作的绝对耗时,但在很多性能问题是发生在两个不同操作之间的性对耗时,比如同一个函数处理1000个元素与处理10000个甚至更多元素的耗时的差别是多少,或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。

性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用。举例子如下:

// split/fib.go

package split

// Fib 是一个计算第n个斐波那契数的函数
func Fib(n int) int {
    if n <= 0 {
        return 0
    } else if n <= 2 {
        return 1
    }
    return Fib(n-1) + Fib(n-2) // 使用递归
}
// split/fib_test.go
package split

import "testing"

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

// 性能比较
func BenchmarkFib5(b *testing.B) { benchmarkFib(b, 5) }
func BenchmarkFib50(b *testing.B) { benchmarkFib(b, 50) }
func BenchmarkFib500(b *testing.B) { benchmarkFib(b, 500) }
func BenchmarkFib500000000(b *testing.B) { benchmarkFib(b, 500000000) }

执行 go test -bench=Fib

PS F:\Learning\test\split> go test -bench=Fib
goos: windows
goarch: amd64
pkg: Learing/test/split
cpu: Intel(R) Core(TM) i5-4460  CPU @ 3.20GHz
BenchmarkFib5-4                 385520368                3.169 ns/op
BenchmarkFib50-4                35705040                31.33 ns/op
BenchmarkFib500-4                6423651               183.3 ns/op
BenchmarkFib500000000-4                7         167491157 ns/op
PASS
ok      Learing/test/split      5.692s

注意:默认情况下,每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增加,并且函数再次运行。

如最终 BenchmarkFib500000000只运行了 7 次,像这种情况下我们应该使用 -benchtime 增加最小基准时间,以产生更准确的结果。如下:

PS F:\Learning\test\split> go test -bench=Fib50000000 -benchtime=10s
goos: windows
goarch: amd64
pkg: Learing/test/split
cpu: Intel(R) Core(TM) i5-4460  CPU @ 3.20GHz
BenchmarkFib500000000-4               72         166507756 ns/op
PASS
ok      Learing/test/split      12.372s

3.4. 重置时间

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

func benchmarkFib(b *testing.B, n int) {
    time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作
    b.ResetTimer()              // 重置计时器

    for i := 0; i < b.N; i++ {
        Fib2(n)
    }
}

3.5. 并行测试

RunParallel 会创建多个 goroutine,并将 b.N 分配给这些 goroutine 执行,其中 goroutine 数量的默认值为 GOMAXPROCS。
若要增加非CPU受限基准测试的并行性,可在 RunParallel 之前调用 SetParallelism 设置并行数。还可以通过在测试命令后添加 -cpu 参数如 go test -bench=. -cpu 1 来指定使用的CPU数量。

func BenchmarkFibParallel(b *testing.B) {
    //b.SetParallelism(2) // 设置CPU使用数
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Fib(100)
        }
    })
}
PS F:\Learning\test\split> go test -bench=Fib -cpu 2
goos: windows
goarch: amd64
pkg: Learing/test/split
cpu: Intel(R) Core(TM) i5-4460  CPU @ 3.20GHz
BenchmarkFib-2                  187356174                6.312 ns/op
BenchmarkFibParallel-2          11944492                99.56 ns/op
PASS
ok      Learing/test/split      3.381s

4. Setup 与 Teardown

测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行拆卸(teardown)。
SS
若测试文件包含 func TestMain(m testing.M),那么生成的测试用例会先调用 TestMain(m),然后运行具体测试。退出测试时应该使用m.Run的返回值作为参数调用os.Exit。

func TestMain(m *testing.M) {
    fmt.Println("测试-TestMain Setup")  // 测试之前的做一些设置
    // 如果 TestMain 使用了 flags,这里应该加上 flag.Parse()
    retCode := m.Run()   // 执行测试
    fmt.Println("测试-TestMain Teardown")   // 测试之后做一些拆卸工作
    os.Exit(retCode)  // 退出测试
}
PS F:\Learning\test\split> go test -bench=Split
测试-TestMain Setup
goos: windows
goarch: amd64
pkg: Learing/test/split
cpu: Intel(R) Core(TM) i5-4460  CPU @ 3.20GHz
BenchmarkSplit-4         4676877               254.5 ns/op
BenchmarkSplitHigh-4    10206516               113.2 ns/op
PASS
测试-TestMain Teardown
ok      Learing/test/split      3.754s

5. 示例函数

示例函数能作为文档直接使用

格式如下:

func ExampleName() {
	// ...
}

示例:

func ExampleSplit() {
    fmt.Println(split.Split("a:b:c", ":"))
    fmt.Println(split.Split("x||y||z", "||"))

    // Output:
    // [a b c]
    // [x y z]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值