Go的单元测试与基准测试

150 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天,简单地了解了go的单元测试相关知识以及应用

单元测试

概念

Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试。testing框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题:

  1. 确保每个函数是可运行的,并且运行结果是正确的
  2. 确保写出来的代码性能是好的
  3. 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定

注意事项

  1. 测试用例文件必须以 _test.go结尾。比如cal_test.go,cal不是固定的
  2. 测试用例函数必须以Test开头,一般来说就是Test+被测试的函数名,比如TestAddUpper
  3. TestAddUpper(t testing.T) 的形参类型必须是testing.T
  4. 一个测试用例文件中,可以有多个测试用例函数,比如TestAddUpper,TestSub
  5. 测试单个文件,一定要带上被测试的原文件,即假设在一个文件夹下面有两个测试文件,一个叫cal_test.go,一个叫sub_test.go,以及一个包含被测试函数的文件,cal.go。此时我只想测试cal_test.go,而不测试sub_test.go,若直接执行go test,则默认执行所有测试文件,因此执行命令go test -v cal_test.go cal.go(要带上被测函数所在的文件)
  6. 测试单个方法 go test -v -test.run TestAddUpper

使用

每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:

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

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

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

例子

在example1.go中定义一个函数

 package unitTest
 ​
 func add(a int, b int) int {
     return a + b
 }

在example1.go同级目录下定义一个example1_test.go文件

 package unitTest
 ​
 import "testing"
 ​
 func TestAdd(t *testing.T) {
    got := add(3, 3)
    want := 6
    if got != want {
       t.Errorf("expected:%v, got:%v", want, got)
    }
 }

在这两个文件的目录下执行go test命令即可进行测试,可以通过-v参数来查看测试函数名称和运行时间

image-20230127220859006.png

还可以在go test命令后添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行。

image-20230127221036753.png

基准测试

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

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

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

为Split函数编写基准测试函数

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

基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试

image-20230127232131162.png

注意: 若想运行所有基准测试函数,需要在-bench后面加上点,有两种写法,写法一:-bench=".",写法二:-bench .。不能直接-bench=.,在Windows下直接这样子写的话结果是不会运行任何基准测试的用例

6041946和194.6ns/op表示每次调用Split函数耗时194.6ns,这个结果是6041946次调用的平均值

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

image-20230127232639055.png

112 B/op 表示每次操作内存分配了112字节,3 allocs/op 则表示每次操作进行了3次内存分配。

重置时间

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

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

image-20230127233956210.png

并行测试

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

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