简单介绍

go test 子命令是 Go 语言包的测试驱动程序,在一个包目录中,以*_test.go 命名方式的文件,是 go test 编译的目标(不是 go build)

在*_test.go 文件中,三种函数需要特殊对待,即功能测试函数、基准测试函数和示例函数:

  • 功能测试函数:以 Test 前缀命名的函数,用来检测一些程序逻辑的正确性
  • 基准测试函数:以 Benchmark 开头,用来测试某些操作的性能
  • 示例函数:以 Example 开头,用来提供机器检查过的文档
  • 参考代码 1
  • 参考代码 2

简单使用

功能函数

  • 首先,在某一工程目录下创建两个文件:divide.go(即源码文件)和 divide_test.go(即单元测试文件),因为 go test 命令只能在一个相应的目录下执行所有文件

divide.go源码文件,创建一个 calc 包,并实现一个除法运算

package calc

import (
  "errors"
)

func Division(a, b float64) (float64, error) {
  if b == 0 {
    return 0, errors.New("除数不能为 0")
    }
    return a / b, nil
}

单元测试

divide_test.go 测试单元文件

package calc

import  "testing"

/*功能测试函数*/
func TestDivision(t *testing.T) {
    // 除数不为零的情况
    if ans, err := Division(2, 1); err == nil {
        t.Logf("测试通过,结果为:%f\n", ans) //测试用例预期的结果
    } else {
        t.Errorf("测试不通过:%s\n", err)    //非预期结果
    }
    //除数为零
    if _, err := Division(7, 0); err == nil {
        t.Errorf("测试不通过:%s\n", err)   //非预期结果
    } else {
        t.Logf("测试通过:%s\n", err)       //测试用例预期的结果
    }
}

注意事项

  • 文件名必须是_test.go 结尾的,这样在执行 go test 的时候才会执行到相应的代码
  • 你必须 import testing 这个包
  • 测试用例会按照源代码中写的顺序依次执行
  • 测试格式:func TestXxx (t *testing.T),Xxx 部分可以为任意的字母数字的组合,但是首字母一定为大写字母
  • 函数中通过调用 testing.T 的一些方法来说明测试通过或不通过:
    1. 调用 t.Error() 或 t.Errorf() 方法记录日志并标记测试失败,测试函数中的某条测试用例执行结果与预期不符时使用
    2. Log 和 Logf 方法用于日志输出,默认只输出错误日志,如果要输出全部日志需要使用-v
    3. Fail 标记用例失败,但继续执行当前用例。FailNow 标记用例失败并且立即停止执行当前用例,继续执行下一个(默认按书写顺序)用例
    4. Error 等价于 Log 加 Fail,Errorf 等价于 Logf 加 Fail
    5. 使用 t.Fatal() 和 t.Fatalf() 方法,在某条测试用例失败后就_跳出该测试函数_
    6. SkipNow 标记跳过并停止执行该用例,继续执行下一个用例。Skip 等价于 Log 加 SkipNow,Skipf 等价于 Logf 加 SkipNow,Skipped 返还用例是否被跳过 if testing.Short() {t.Skip(“slow test; skipping”)}
    7. t.Parallel() 示意该测试用例和其它并行用例(也调用该方法的)一起并行执行
    8. func (c *T) Run(name string, f func(t *T)) bool
  • 和普通的 Golang 源代码一样,测试代码中也能定义 init 函数,init 函数会在引入外部包、定义常量、声明变量之后被自动调用,可以在 init 函数里编写测试相关的初始化代码

常用的测试命令及参数介绍

  • go test [-v] [-run]
  • go test: 下执行是不会显示测试通过的信息的,但会显示测试不通过的测试用例名称和执行时间
  • -v: 可以输出包中每个测试用例的名称和执行时间(go test -v)
  • -run : 匹配一个正则表达式,它可以运行指定的测试函数(go test –v –run=”Division”)

执行结果

$ go test
  PASS
  ok      git.cloud.top/kingfs/gotest/src 0.175s
$ go test -v
  === RUN   TestDivision
  --- PASS: TestDivision (0.00s)
      test_test.go:11: 测试通过结果为2.000000
      test_test.go:19: 测试通过除数不能为 0
  PASS
  ok      git.cloud.top/kingfs/gotest/src 0.165s

常用的测试方法

基于表的测试方法

  • 一般格式:
var ** = []struct{
  name T
  want T
}{
  {##,##},
  ...
}
  • 基于表的测试方法在 Go 里面很常见,根据需要添加新的表项很直观
  • 示例:
func TestDivision_table(t *testing.T) {
  var tests = []struct {
          a    float64
          b    float64
          want float64
          }{
          {100, 4, 25},
          {6, 2, 3},
          {0, 100, 0},
          {3, 0, math.NaN()},
        }

    for _, test := range tests {
      if ans, err := Division(test.a, test.b); math.IsNaN(test.want) == false && test.want == ans {
        t.Logf("测试通过,结果为:%f\n", ans) //测试用例预期的结果
        } else if math.IsNaN(test.want) == false && test.want != ans {
          t.Errorf("测试不通过:%s\n", err) //非预期结果
          } else if math.IsNaN(test.want) == true && err != nil {
            t.Logf("测试通过:%s\n", err)
            } else if math.IsNaN(test.want) == true && err == nil {
              t.Errorf("测试不通过:%s\n", err)
              }}}

随机测试

这里随机测试就是通过构建随机输入来扩展测试的覆盖范围(和随机测试工具** go-fuzz **不一样) 例子见 test_test.go:通过通过构建伪随机数生成随机回文字符串,来测试函数 随机数生成参考链接: 1 2

白盒测试

黑盒测试:假设测试者对包的了解仅通过公开的 API 和文档,而包的内部逻辑则是不透明的 白盒测试:可以访问源码内部函数和数据结构 在白盒测试中对全局变量的更新有时会存在风险,我们需要恢复这个测试全局变量的值,这样后面的测试才不会受影响,这种情况可以使用 defer 以这种方式来使用全局变量是安全的,因为 go test 一般不会并发执行多个测试 defer 参考: defer1 defer2

  • 例子:
//example.go
package mypackage
import "fmt"
var Flag int=1

func mulFlag(){
  if Flag==1{Flag*=5}
  fmt.Println(Flag)
}
func AddFlag(){
  if Flag==1{Flag+=1}
  fmt.Println(Flag)
}

//example_test.go
package mypackage
import "testing"
func TestMulFlag(t *testing.T){
  // saved := Flag
    // defer func() { Flag = saved }()
  mulFlag()
}
func TestAddFlag(t *testing.T){
  AddFlag()
}

外部测试包/黑盒测试

上面的测试和源码都属于同一个包,外部测试包有一个额外的后缀**_test**,_test 告诉 go test 工具它应该单独的编译一个包。它可以解决包循环引用问题 (GO 规范禁止循环引用)例如:在被测试的包 “a” 被 “b” 导入,并且 “a“ 的测试也需要导入 ”b“ 时 - 测试可以被移动到 “a_test“ 包,然后可以(同时)导入 “a” 和 “b”,这样就没有循环导入的问题。
  • 例子如下:
    //example.go
    package myexample
    
    import (
        "errors"
    )
    
    func Division(a, b float64) (float64, error) {
        if b == 0 {
          return a / b, errors.New("除数不能为 0")
        }
    
        return a / b, nil
    }
    //example_test.go(与 example.go 不在同一目录)
    package myexample_test
    
    import (
        "fmt"
        . "test"                       //这里表示的是路径,导入实际包名为 myexample
        "testing"
    )
    
    func TestDvision(t *testing.T) {
      fmt.Println(Division(4, 2))
    }
    
    • 有时候,外部测试包需要对待测试包拥有特殊的访问权限,为了避免循环引用,一个白盒测试必须存在于一个单独的包中
    • 解决小技巧:在包测试文件_test.go 中添加一些函数申明,将包的内部功能暴露给外部测试,如果一个源文件存在且只是为测试提供包的一个"后门",他们一般称作 export_test.go, 这种方式在源码中很常见

子用例

```GO
func TestRunSuits(t *testing.T) { 
  t.Run("A=1", func(t *testing.T) { t.Log("sub test A=1") }) 
  t.Run("A=2", func(t *testing.T) { t.Log("sub test A=2") }) 
  t.Run("B=1", func(t *testing.T) { t.Log("sub test B=1") }) 
  t.Run("B=2", func(t *testing.T) { t.Log("sub test B=2") }) 
}
```

Run 运行一个名为 name 的子用例,返回该子用例是否通过。可以通过-run exp 正则表达式参数指定要运行的子用例,例如上面例子中可以通过 go test -v -run “/A” 运行两个子用例,正则表达式的顶层以 / 开头。子用例的引入方便更好的对测试用例进行组织 参考链接

web 中 Handler 函数测试

  • 示例代码
//test.go 中
import (
"log"
"fmt"
  "net/http"
)

//hangdle
func GetUrlPath(w http.ResponseWriter, r *http.Request) {
    if r != nil {
  	  fmt.Fprintf(w, "%s", r.URL.Path)
    }
}

//创建 web 服务
func CreateWeb() {
  http.HandleFunc("/", GetUrlPath)
  err := http.ListenAndServe(":1234", nil)
  if err != nil {
  	log.Fatal("ListenAndServe:", err)
    }
}

//test_test.go 中
import (
  "net/http"
  "net/http/httptest"
  "testing"
)

func TestGetUrlPath(t *testing.T) {
  //创建一个请求
  req, err := http.NewRequest("GET", "http://127.0.0.1:1234/aaa/bbb", nil)
  if err != nil {
  	t.Error("NewRequest:", err)
  }
  //创建一个接受响应
  ww := httptest.NewRecorder()
  //调用 handle
  GetUrlPath(ww, req)
  //检测返回状态
  if status := ww.Code; status != http.StatusOK {
  	t.Errorf("handle get wrong status:%v,want:%v\n", status, http.StatusOK)
  }
  //检测返回数据
  ans := "/aaa/bbb"
  if str := ww.Body.String(); str != ans {
  	t.Errorf("---got:%s,want:%s\n", str, ans)
  }
}

TestMain 的使用

有时会遇到这样的场景:
进行测试之前需要初始化操作(例如打开连接),测试结束后,需要做清理工作(例如关闭连接)等等。这个时候就可以使用 TestMain()
  • 例子:
    func TestMain(m *testing.M) {
      fmt.Println("begin")
      m.Run()
      fmt.Println("end")
    }
    

Golang 中覆盖率测试

  • 执行 go tool cover 可以看到 cover 工具的使用和参数信息
  • 覆盖率的统计模式有三种:
    1. set : 缺省模式,只记录语句是否被执行过
    2. count : 记录语句被执行的次数
    3. atomic : 记录语句被执行的次数,并保证在并发执行时的正确性
  • 覆盖率测试一般步骤:
    1. 先用 -coverprofile 标记来运行测试 $ go test -run=TestDivision_table -coverprofile=c.out

      若想改变覆盖率统计模式可以用参数:-covermode=## 来标记指定 profile 文件:c.out 第一排记录着覆盖率的统计模式

    2. 若只想得到统计结果只需要参数:-cover 即可
      $ go test -cover
        PASS
        coverage: 100.0% of statements
        ok      git.cloud.top/kingfs/gotest/src 0.134s
      
    3. 生成数据后,我们来处理生成的日志,可以生成一个 HTML 报告,并在新的浏览器中打开它
      $ go tool cover -html=c.out
      
    4. 生成 HTML 报告文件
      $ go tool cover -html=c.out -o coverage.html
      

Benchmark 函数

  • 概述: 基准测试即压力测试就是在一定的工作负载下检测程序性能的一种方法
  • 文件名也必须以_test.go 结尾
  • 基准测试格式:func BenchmarkXXX(b *testing.B) { … }
  • go test 不会默认执行压力测试的函数,如果要执行压力测试需要带上参数-test.bench,语法:-test.bench=“test_name_regex”, 例如 go test -test.bench=".*“表示测试全部的压力测试函数
  • 在压力测试用例中,请记得在循环体内使用 testing.B.N, 以使测试可以正常的运行
  • testing.B 类型的常用方法的用法
    1. 在函数中调用 t.ReportAllocs() ,启用内存使用分析
    2. 通过 b.StopTimer() 、b.ResetTimer() 、b.StartTimer() 来停止、重置、启动 时间经过和内存分配计数
    3. 调用 b.SetBytes() 记录在一个操作中处理的字节数
    4. 通过 b.RunParallel() 方法和 *testing.PB 类型的 Next() 方法来并发执行被测对象 参考链接
  • 基准测试函数列子
//压力测试
func Benchmark_Division(b *testing.B) {
      for i := 0; i < b.N; i++ {
        Division(7, 3)
      }
}

//压力测试排除一些初始化工作时间
func Benchmark_Division_Update(b *testing.B) {
      b.StopTimer()
      b.StartTimer()
      b.ReportAllocs()
      for i := 0; i < b.N; i++ {
        Division(7, 3)
      }
}
  • 执行命令 :
    • go test -bench=”.*"
    • go test -bench=“Benchmark_Division_Update” -count=5 (-count 指定执行次数)
    • go test –bench=. –benchmem 参数:-benchtime 可以控制 benchmark 的运行时间(-benchtime=”3s”)
  • 执行结果 :
    $ go test -bench=.
       goos: windows
       goarch: amd64
       pkg: git.cloud.top/kingfs/gotest/src
       Benchmark_Division-4            2000000000               0.32 ns/op
       Benchmark_Division_Update-4     2000000000               0.32 ns/op            0 B/op          0 allocs/op
       PASS
       ok      git.cloud.top/kingfs/gotest/src 1.498s
    
    $ go test -bench=. -benchmem
       goos: windows
       goarch: amd64
       pkg: git.cloud.top/kingfs/gotest/src
       Benchmark_Division-4            2000000000               0.33 ns/op            0 B/op          0 allocs/op
       Benchmark_Division_Update-4     2000000000               0.34 ns/op            0 B/op          0 allocs/op
       PASS
       ok      git.cloud.top/kingfs/gotest/src 1.544s
    
  • 结果分析 : 表示函数 Benchmark_Division* ,-4 表示 4 个 CUP 线程执行,2000000000 共执行次数,0.32 ns/op 表示每次执行耗时 0.32 纳秒,0 B/op 表示每次执行分配 0 字节内存,0 allocs/op 表示每次执行分配 0 次对象 函数中 b.ReportAllocs() 使得结果 repor 中包含内存分配信息,这个与参数“-benchmem”效果一样

性能剖析

主要包括 3 类性能剖析:

  • CPU 性能剖析:识别出执行过程需要 CPU 最多的函数
  • 堆性能剖析:识别出负责分配最多内存的语句
  • 阻塞性能剖析:识别出那些阻塞协程最久的操作
  1. CPU 性能剖析 生成性能剖析的可执行文件和性能剖析日志的格式如下:

go test -run=文件名字 -bench=bench 名字 -cpuprofile=cprofile 文件名称 文件夹

  • 执行命令

    $ go test -run=NONE -bench=. -cpuprofile=cpu.out
      goos: windows
      goarch: amd64
      pkg: git.cloud.top/kingfs/gotest/src
      Benchmark_Division-4            2000000000               0.33 ns/op
      Benchmark_Division_Update-4     2000000000               0.33 ns/op            0 B/op          0 allocs/op
      PASS
      ok      git.cloud.top/kingfs/gotest/src 1.584s
    

    在执行命令后在工程目录中你会发现多出了两个文件:*.test 的可执行文件和 profile 文件

    $ go tool pprof *.test  profile 文件      (*进入交互模式*)
    
    $ go tool pprof src.test.exe cpu.out
      File: src.test.exe
      Type: cpu
      Time: Jun 21, 2018 at 7:39pm (CST)
      Duration: 1.55s, Total samples = 1.43s (91.96%)
      Entering interactive mode (type "help" for commands, "o" for options)
      (pprof)
    
  • 在交互模式下常用到命令有:top web svg pdf text 等

  • web 命令会生成性能剖析图,并以 web 界面形式展示

    注:要生成性能剖析图要先下载安装 graphviz 工具

    graphviz 下载链接 解压包至某一目录,并将解压后的bin目录添加至环境变量path中 验证,命令行输入:dot -version

  • $ go tool pprof –web *.test profile 文件 (等同于交互模式下的 web 命令)
  • $ go tool pprof –svg profile 文件 > *.svg (等同于交互模式下的 pdf 命令)
  • $ go tool pprof –pdf profile 文件 > *.pdf (等同于交互模式下的 pdf 命令) 这两命令可以将性能分析图生成 svg 和 pdf 格式的文件
  • 交互模式下 top 命令各字段意思:
    • flat : 采样点落在该函数中的次数
    • flat%: 采样点落在该函数中的百分比
    • sum% : 上一项的累积百分比
    • cum : 采样点落在该函数,以及被它调用的函数中的总次数
    • cum% : 采样点落在该函数,以及被它调用的函数中的总次数百分比
    • 最后一列表示函数
  1. 堆性能剖析
    • go test -run=文件名字 -bench=bench 名字 –blockprofile=*.out 文件夹 go tool pprof 操作和 cup 性能分析一样
  2. 阻塞性能剖析
    • go test -run=文件名字 -bench=bench 名字 –memprofile=*.out 文件夹

Example 函数

有三大作用:

  1. 作为文档
  2. godoc 会将示例函数和所演示包相关联,go doc 工具会解析示例函数的函数体作为对应 包/函数/类型/类型的方法 的用法
  3. 提供手动试验代码
  • godoc 详解请参考 godoc

  • 例子:

func ExampleDivision() {
  Division(4, 2)
  //Output
  //2
}

go test 的参数解读

  • 格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]
  • 参数解读
  • -c : 编译 go test 成为可执行的二进制文件,但是不运行测试。
  • -i : 安装测试包依赖的 package,但是不运行测试。
  • 关于 build flags,调用 go help build,这些是编译运行过程中需要使用到的参数,一般设置为空
  • 关于 packages,调用 go help packages,这些是关于包的管理,一般设置为空
  • 关于 flags for test binary,调用 go help testflag,这些是 go test 过程中经常使用到的参数
  • -test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。
  • -test.run pattern: 只跑哪些单元测试用例
  • -test.bench patten: 只跑那些性能测试用例
  • -test.benchmem : 是否在性能测试的时候输出内存情况
  • -test.benchtime t : 性能测试运行的时间,默认是 1s
  • -test.cpuprofile cpu.out : 是否输出 cpu 性能分析文件
  • -test.memprofile mem.out : 是否输出内存性能分析文件
  • -test.blockprofile block.out : 是否输出内部 goroutine 阻塞的性能分析文件
  • -test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是 profile 中一个 sample 代表的内存大小。默认是设置为 512 * 1024 的。如果你将它设置为 1,则每分配一个内存块就会在 profile 中有个打点,那么生成的 profile 的 sample 就会非常多。如果你设置为 0,那就是不做打点了。 你可以通过设置 memprofilerate=1 和 GOGC=off 来关闭内存回收,并且对每个内存块的分配进行观察。
  • -test.blockprofilerate n: 基本同上,控制的是 goroutine 阻塞时候打点的纳秒数。默认不设置就相当于-test.blockprofilerate=1,每一纳秒都打点记录一下
  • -test.parallel n : 性能测试的程序并行 cpu 数,默认等于 GOMAXPROCS。GOMAXPROCS
  • -test.timeout t : 如果测试用例运行时间超过 t,则抛出 panic
  • -test.cpu 1,2,4 : 程序运行在哪些 CPU 上面,使用二进制的 1 所在位代表,和 nginx 的 nginx_worker_cpu_affinity 是一个道理
  • -test.short : 将那些运行时间较长的测试用例运行时间缩短