简单介绍

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

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

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

简单使用

Test函数

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

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

  package test

  import (
    "errors"
  )

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

test_test.go 测试单元文件

  • 注意事项:

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

    package test
    
    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)       //测试用例预期的结果
        }
    }
    

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

  • 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,这种方式在源码中很常见

      子用例

      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最多的函数
    • 堆性能剖析:识别出负责分配最多内存的语句
    • 阻塞性能剖析:识别出那些阻塞协程最久的操作
  • 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 CPU性能剖析图

    • $ 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% :采样点落在该函数,以及被它调用的函数中的总次数百分比
      • 最后一列表示函数
  • 堆性能剖析

    • go test -run=文件名字 -bench=bench名字 –blockprofile=*.out 文件夹 go tool pprof 操作和cup性能分析一样
  • 阻塞性能剖析

    • go test -run=文件名字 -bench=bench名字 –memprofile=*.out 文件夹

      Example函数

      有三大作用:

  • 作为文档

  • godoc会将示例函数和所演示包相关联,go doc 工具会解析示例函数的函数体作为对应 包/函数/类型/类型的方法 的用法

  • 提供手动试验代码 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 : 将那些运行时间较长的测试用例运行时间缩短