golang中的单元测试

golang中的单元测试
文章图片

优秀的代码习惯一定是伴随着单元测试的,这也是go语言设计的哲学;
国外的很多公司很多优秀的程序员都比较重视TDD,但是在国内十分少见;(TDD:测试驱动开发(test driven devlopment))
无论如何,学习并使用golang的单元测试,不是浪费时间,而是让你的代码更加优雅健硕!
golang中的单元测试
文章图片

测试文件
文件名以_test.go为后缀的文件均为测试代码,都会被go test测试捕捉,不会被go build编译;
测试函数
测试文件中的三种函数类型:

  • 单元测试函数:函数名前缀Test;测试程序的逻辑
  • 基准函数:函数名前缀Benchmark;测试函数的性能
  • 示例函数:函数名前缀Example;会出现在godoc中的,为文档提供示例文档
测试命令
Go语言中的测试依赖go test命令;在此命令下添加各种不同的参数以实现不同目的的测试;后面会一一介绍;
go test命令会遍历所有的*_test.go文件中符合上述命名规则的测试函数
然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件;
接下来分别介绍单元测试函数、基准函数、示例函数:
单元测试函数
  • 单元测试函数的格式:
    1. func TestName(t *testing.T) {}
    2. 函数的名字必须以Test开头,可选的后缀名必须以大写字母开头
    3. 每个测试函数必须导入testing包;关于testing包中的方法可以去看一下源码;
    4. 参数t用于报告测试失败和附加的日志信息
  • 一个简单的测试函数示例:将输出的结果预期的结果进行比较
    1. 创建业务函数
      // 文件split/split.go:定义一个split的包,包中定义了一个Split函数 package splitimport "strings"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 }

    2. 创建测试文件
      // 文件split/split_test.go:创建一个split_test.go的测试文件 package splitimport ( "reflect" "testing" )// 单元测试函数 // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数 // 1. 直接调用业务函数 // 2. 定义期望结果 // 3. 比较实际结果和期望结果 func TestSplit(t *testing.T) { got := Split("a:b:c", ":")// 调用程序并返回程序结果 want := []string{"a", "b", "c"} // 期望的结果if !reflect.DeepEqual(want, got) { // 因为slice不能直接比较,借助反射包中的方法比较 t.Errorf("expected:%v, got:%v", want, got) // 如果测试失败输出错误提示 } }// 提供一个失败的单元测试 func TestSplitFail(t *testing.T) { got := Split("abcd", "bc") want := []string{"a", "d"}if !reflect.DeepEqual(want, got) { t.Errorf("expected:%v, got:%v", want, got) } }// 基准测试函数 func BenchmarkSplit(b *testing.B) {}// 示例函数 func ExampleSplit() {}

    3. 执行测试命令
      进入split目录下,直接运行 go test 命令即可;
      如果运行go test -v的话,可以看到更详细的输出结果:知道哪个测试函数没有通过,错在哪里
      === RUNTestSplit --- PASS: TestSplit (0.00s) === RUNTestSplitFail split_test.go:28: expected:[a d], got:[a cd] --- FAIL: TestSplitFail (0.00s) FAIL exit status 1 FAILgotest/split0.001s

  • 其他go test命令
    go test -run=?run对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行;
    // 比如以上代码执行命令:go test -v -run=Fail // 表示本次只运行 能正则匹配到Fail的 测试函数 === RUNTestSplitFail split_test.go:28: expected:[a d], got:[a cd] --- FAIL: TestSplitFail (0.00s) FAIL exit status 1 FAILgotest/split0.001s

    go test -short:跳过测试函数中包含testing.Short()函数的测试函数;一般用于跳过执行起来太耗时的测试函数;比如:
    // 修改以上示例代码中的TestSplitFail函数如下 func TestSplitFail(t *testing.T) { if testing.Short() { t.Skip("short模式下会跳过该测试用例") } got := Split("abcd", "bc") // 调用程序并返回程序结果 want := []string{"a", "d"} // 期望的结果if !reflect.DeepEqual(want, got) { // 因为slice不能直接比较,借助反射包中的方法比较 t.Errorf("expected:%v, got:%v", want, got) // 如果测试失败输出错误提示 } }// 然后执行命令`go test -v -short`打印如下结果: === RUNTestSplit --- PASS: TestSplit (0.00s) === RUNTestSplitFail split_test.go:25: short模式下会跳过该测试用例 --- SKIP: TestSplitFail (0.00s) PASS okgotest/split0.002s

    go test -cover测试覆盖率:覆盖率是指测试代码覆盖的业务代码的占比;
    go test -cover -coverprofile=c.out将覆盖率相关的信息输出到当前文件夹下面的c.out文件中;
    再然后执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告;
  • 子测试:对多组测试用例能够清晰准确的进行错误定位
    Go1.7+中新增了子测试,我们可以按照如下方式使用t.Run执行子测试:
    func TestSplit(t *testing.T) { type test struct { input string sepstring want[]string } // 定义多组测试用例 tests := map[string]test{ "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{"河有", "又有河"}}, }// t.Run就是子测试 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("expected:%#v, got:%#v", tc.want, got) } }) } }// 执行 go test -v === RUNTestSplit === RUNTestSplit/simple === RUNTestSplit/wrong_sep === RUNTestSplit/more_sep split_test.go:34: expected:[]string{"a", "d"}, got:[]string{"a", "cd"} === RUNTestSplit/leading_sep split_test.go:34: expected:[]string{"河有", "又有河"}, got:[]string{"", "\xb2\x99河有", "\xb2\x99又有河"} --- FAIL: TestSplit (0.00s) --- PASS: TestSplit/simple (0.00s) --- PASS: TestSplit/wrong_sep (0.00s) --- FAIL: TestSplit/more_sep (0.00s) --- FAIL: TestSplit/leading_sep (0.00s) FAIL exit status 1 FAILgotest/split0.002s

基准函数
在一定的工作负载之下检测程序性能;
基本格式如:func BenchmarkName(b *testing.B){}
  • Benchmark为前缀,后面跟着首字母大写
  • 必须要有testing.B类型的参数;
  • 基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的(见后面使用示例)
  • 关于testing.B的方法可以参考源码
  • 一个简单的基准函数的示例:
    1. 创建测试函数(继续接着上面的示例代码)
      func BenchmarkSplit(b *testing.B) { // 注意b.N for i := 0; i < b.N; i++ { Split("沙河有沙又有河", "沙") } }

    2. 执行测试命令-bench=$正则匹配
      // go test -bench=Split goos: windows goarch: amd64 pkg: go-test/split cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz BenchmarkSplit-44017925311.9 ns/op PASS okgo-test/split1.976s

      以上输出了电脑的一些信息;
      其中BenchmarkSplit-4中的4表示GOMAXPROCS的值;
      4017925表示调用该函数的次数;
      311.9 ns/op调用该函数的平均耗时;
      // `go test -bench=Split -benchmem`增加了内存分配的统计数据 goos: windows goarch: amd64 pkg: go-test/split cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz BenchmarkSplit-43995856300.9 ns/op112 B/op3 allocs/op PASS okgo-test/split1.890s

      其中112 B/op表示每次操作内存分配了112字节;
      3 allocs/op表示每次操作进行了3次内存分配;
  • 性能比较函数
    1. 基准函数只能给出绝对耗时;实际中我们可能想知道不同操作的相对耗时;
    2. 性能比较函数通常是一个带有参数的函数,被多个不同的Benchmark函数传入不同的值来调用;
  • 一个简单的基准测试中的性能比较函数示例:
    1. 创建一个fib目录,并新建一个fib.go文件
      package fib// Fib 是一个计算第n个斐波那契数的函数 func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) }

    2. 同目录下创建一个测试函数文件fib_test.go
      package fibimport ( "testing" )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) }

    3. 执行测试命令
      // go test -bench=Fib goos: windows goarch: amd64 pkg: go-test/fib cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz BenchmarkFib1-44704786182.393 ns/op BenchmarkFib2-41787733996.737 ns/op BenchmarkFib3-410000000012.60 ns/op BenchmarkFib10-43025942421.8 ns/op BenchmarkFib20-42479255344 ns/op BenchmarkFib40-42724560000 ns/op PASS okgo-interview/fib11.675s

      BenchmarkFib40-4 2 724560000 ns/op:第一个是对应的比较函数;第二个是执行的次数;第三个是执行的平均时间;
      由此可见fib函数入参值越大,函数执行的效率越低;
  • 基准测试命令除了以上这些外还有一些其他的参数做其他用处:比如这些参数benchtime,ResetTimer,RunParallel,SetParallelism,cpu,setup,teardown,TestMain,可以了解一下
示例函数
示例函数能够作为文档直接使用,例如基于web的godoc中能把示例函数与对应的函数或包相关联
golang中的单元测试
文章图片

坚持每日输出go开发+面试题+算法+工作经验等后端相关技术
关于我今年的计划请查看:flag-2022
【golang中的单元测试】更多博客内容请查看bigshake

    推荐阅读