Go语言基础之单元测试

更新、更全的《Go从入门到放弃》的更新网站,更有python、go、人工智能教学等着你:http://www.javashuo.com/article/p-mxrjjcnn-hn.htmlpython

不写测试的开发不是好程序员。我我的很是崇尚TDD(Test Driven Development)的,然而惋惜的是国内的程序员都不太关注测试这一部分。 这篇文章主要介绍下在Go语言中如何作单元测试和基准测试。git

1、go test工具

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是相似的,并不须要学习新的语法、规则或工具。程序员

go test命令是一个按照必定约定和组织的测试代码的驱动程序。在包目录内,全部以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。github

*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。golang

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

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

2、测试函数

3、测试函数的格式

每一个测试函数必须导入testing包,测试函数的基本格式(签名)以下:正则表达式

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

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:算法

func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

其中参数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) 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

4、测试函数示例

就像细胞是构成咱们身体的基本单位,一个软件程序也是由不少单元组件构成的。单元组件能够是函数、结构体、方法和最终用户可能依赖的任意东西。总之咱们须要确保这些组件是可以正常运行的。单元测试是一些利用各类方法测试单元组件的程序,它会将结果与预期输出进行比较。

接下来,咱们定义一个split的包,包中定义了一个Split函数,具体实现以下:

// 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)

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

在当前目录下,咱们建立一个split_test.go的测试文件,并定义一个测试函数以下:

// split/split_test.go

package split

import (
    "reflect"
    "testing"
)

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

此时split这个包中的文件以下:

split $ ls -l
total 16
-rw-r--r--  1 nickchen121  staff  408  4 29 15:50 split.go
-rw-r--r--  1 nickchen121  staff  466  4 29 16:04 split_test.go

split包路径下,执行go test命令,能够看到输出结果以下:

split $ go test
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s

一个测试用例有点单薄,咱们再编写一个测试使用多个字符切割字符串的例子,在split_test.go中添加以下测试函数:

func TestMoreSplit(t *testing.T) {
    got := Split("abcd", "bc")
    want := []string{"a", "d"}
    if !reflect.DeepEqual(want, got) {
        t.Errorf("excepted:%v, got:%v", want, got)
    }
}

再次运行go test命令,输出结果以下:

split $ go test
--- FAIL: TestMultiSplit (0.00s)
    split_test.go:20: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

这一次,咱们的测试失败了。咱们能够为go test命令添加-v参数,查看测试函数名称和运行时间:

split $ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
    split_test.go:21: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s

这一次咱们能清楚的看到是TestMoreSplit这个测试没有成功。 还能够在go test命令后添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行。

split $ go test -v -run="More"
=== RUN   TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
    split_test.go:21: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

如今咱们回过头来解决咱们程序中的问题。很显然咱们最初的split函数并无考虑到sep为多个字符的状况,咱们来修复下这个Bug:

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)

    for i > -1 {
        result = append(result, s[:i])
        s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

这一次咱们再来测试一下,咱们的程序。注意,当咱们修改了咱们的代码以后不要仅仅执行那些失败的测试函数,咱们应该完整的运行全部的测试,保证不会由于修改代码而引入了新的问题。

split $ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   TestMoreSplit
--- PASS: TestMoreSplit (0.00s)
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

这一次咱们的测试都经过了。

5、测试组

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

func TestSplit(t *testing.T) {
   // 定义一个测试用例类型
    type test struct {
        input string
        sep   string
        want  []string
    }
    // 定义一个存储测试用例的切片
    tests := []test{
        {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        {input: "沙河有沙又有河", sep: "沙", want: []string{"河有", "又有河"}},
    }
    // 遍历切片,逐一执行测试用例
    for _, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("excepted:%v, got:%v", tc.want, got)
        }
    }
}

咱们经过上面的代码把多个测试用例合到一块儿,再次执行go test命令。

split $ go test -v
=== RUN   TestSplit
--- FAIL: TestSplit (0.00s)
    split_test.go:42: excepted:[河有 又有河], got:[ 河有 又有河]
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

咱们的测试出现了问题,仔细看打印的测试失败提示信息:excepted:[河有 又有河], got:[ 河有 又有河],你会发现[ 河有 又有河]中有个不明显的空串,这种状况下十分推荐使用%#v的格式化方式。

咱们修改下测试用例的格式化输出错误提示部分:

func TestSplit(t *testing.T) {
   ...
   
    for _, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("excepted:%#v, got:%#v", tc.want, got)
        }
    }
}

此时运行go test命令后就能看到比较明显的提示信息了:

split $ go test -v
=== RUN   TestSplit
--- FAIL: TestSplit (0.00s)
    split_test.go:42: excepted:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

6、子测试

看起来都挺不错的,可是若是测试用例比较多的时候,咱们是没办法一眼看出来具体是哪一个测试用例失败了。咱们可能会想到下面的解决办法:

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{"河有", "又有河"}},
    }
    for name, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("name:%s excepted:%#v, got:%#v", name, tc.want, got) // 将测试用例的name格式化输出
        }
    }
}

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

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{"河有", "又有河"}},
    }
    for name, tc := range tests {
        t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                t.Errorf("excepted:%#v, got:%#v", tc.want, got)
            }
        })
    }
}

此时咱们再执行go test命令就可以看到更清晰的输出内容了:

split $ go test -v
=== RUN   TestSplit
=== RUN   TestSplit/leading_sep
=== RUN   TestSplit/simple
=== RUN   TestSplit/wrong_sep
=== RUN   TestSplit/more_sep
--- FAIL: TestSplit (0.00s)
    --- FAIL: TestSplit/leading_sep (0.00s)
        split_test.go:83: excepted:[]string{"河有", "又有河"}, got:[]string{"", "河有", "又有河"}
    --- PASS: TestSplit/simple (0.00s)
    --- PASS: TestSplit/wrong_sep (0.00s)
    --- PASS: TestSplit/more_sep (0.00s)
FAIL
exit status 1
FAIL    github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

这个时候咱们要把测试用例中的错误修改回来:

func TestSplit(t *testing.T) {
    ...
    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{"", "河有", "又有河"}},
    }
    ...
}

咱们都知道能够经过-run=RegExp来指定运行的测试用例,还能够经过/来指定要运行的子测试用例,例如:go test -v -run=Split/simple只会运行simple对应的子测试用例。

7、测试覆盖率

测试覆盖率是你的代码被测试套件覆盖的百分比。一般咱们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。

Go提供内置功能来检查你的代码覆盖率。咱们可使用go test -cover来查看测试覆盖率。例如:

split $ go test -cover
PASS
coverage: 100.0% of statements
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s

从上面的结果能够看到咱们的测试用例覆盖了100%的代码。

Go还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

split $ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s

上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中,而后咱们执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告。 cover.png 上图中每一个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

8、基准测试

9、基准测试函数格式

基准测试就是在必定的工做负载之下检测程序性能的一种方法。基准测试的基本格式以下:

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()

10、基准测试示例

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

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

基准测试并不会默认执行,须要增长-bench参数,因此咱们经过执行go test -bench=Split命令执行基准测试,输出结果以下:

split $ go test -bench=Split
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8        10000000               203 ns/op
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       2.255s

其中BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。10000000203ns/op表示每次调用Split函数耗时203ns,这个结果是10000000次调用的平均值。

咱们还能够为基准测试添加-benchmem参数,来得到内存分配的统计数据。

split $ go test -bench=Split -benchmem
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8        10000000               215 ns/op             112 B/op          3 allocs/op
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       2.394s

其中,112 B/op表示每次操做内存分配了112字节,3 allocs/op则表示每次操做进行了3次内存分配。 咱们将咱们的Split函数优化以下:

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

这一次咱们提早使用make函数将result初始化为一个容量足够大的切片,而再也不像以前同样经过调用append函数来追加。咱们来看一下这个改进会带来多大的性能提高:

split $ go test -bench=Split -benchmem
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8        10000000               127 ns/op              48 B/op          1 allocs/op
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       1.423s

这个使用make函数提早分配内存的改动,减小了2/3的内存分配次数,而且减小了一半的内存分配。

11、性能比较函数

上面的基准测试只能获得给定操做的绝对耗时,可是在不少性能问题是发生在两个不一样操做之间的相对耗时,好比同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差异是多少?再或者对于同一个任务究竟使用哪一种算法性能最佳?咱们一般须要对两个不一样算法的实现使用相同的输入来进行基准比较测试。

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

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) }

例如咱们编写了一个计算斐波那契数列的函数以下:

// fib.go

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

咱们编写的性能比较函数以下:

// fib_test.go

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) }

运行基准测试:

split $ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib
BenchmarkFib1-8         1000000000               2.03 ns/op
BenchmarkFib2-8         300000000                5.39 ns/op
BenchmarkFib3-8         200000000                9.71 ns/op
BenchmarkFib10-8         5000000               325 ns/op
BenchmarkFib20-8           30000             42460 ns/op
BenchmarkFib40-8               2         638524980 ns/op
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/fib 12.944s

这里须要注意的是,默认状况下,每一个基准测试至少运行1秒。若是在Benchmark函数返回时没有到1秒,则b.N的值会按1,2,5,10,20,50,…增长,而且函数再次运行。

最终的BenchmarkFib40只运行了两次,每次运行的平均值只有不到一秒。像这种状况下咱们应该可使用-benchtime标志增长最小基准时间,以产生更准确的结果。例如:

split $ go test -bench=Fib40 -benchtime=20s
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib
BenchmarkFib40-8              50         663205114 ns/op
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/fib 33.849s

这一次BenchmarkFib40函数运行了50次,结果就会更准确一些了。

使用性能比较函数作测试的时候一个容易犯的错误就是把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)
}

12、重置时间

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

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

十3、并行测试

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

RunParallel会建立出多个goroutine,并将b.N分配给这些goroutine执行, 其中goroutine数量的默认值为GOMAXPROCS。用户若是想要增长非CPU受限(non-CPU-bound)基准测试的并行性, 那么能够在RunParallel以前调用SetParallelismRunParallel一般会与-cpu标志一同使用。

func BenchmarkSplitParallel(b *testing.B) {
    // b.SetParallelism(1) // 设置使用的CPU数
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Split("沙河有沙又有河", "沙")
        }
    })
}

执行一下基准测试:

split $ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8                10000000               131 ns/op
BenchmarkSplitParallel-8        50000000                36.1 ns/op
PASS
ok      github.com/Q1mi/studygo/code_demo/test_demo/split       3.308s

还能够经过在测试命令后添加-cpu参数如go test -bench=. -cpu 1来指定使用的CPU数量。

十4、Setup与TearDown

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

十5、TestMain

经过在*_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的示例以下:

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

十6、子测试的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)
            }
        })
    }
}

测试结果以下:

split $ 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      github.com/Q1mi/studygo/code_demo/test_demo/split       0.006s

十7、示例函数

十8、示例函数的格式

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

func ExampleName() {
    // ...
}

十9、示例函数示例

下面的代码是咱们为Split函数编写的一个示例函数:

func ExampleSplit() {
    fmt.Println(split.Split("a:b:c", ":"))
    fmt.Println(split.Split("沙河有沙又有河", "沙"))
    // Output:
    // [a b c]
    // [ 河有 又有河]
}

为你的代码编写示例代码有以下三个用处:

  1. 示例函数可以做为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联。

  2. 示例函数只要包含了// Output:也是能够经过go test运行的可执行测试。

    ```bash split $ go test -run Example PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s ```
  3. 示例函数提供了能够直接运行的示例代码,能够直接在golang.orggodoc文档服务器上使用Go Playground运行示例代码。下图为strings.ToUpper函数在Playground的示例函数效果。 example.png

二10、练习题

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