Hits

go test 测试你的代码

go test 测试你的代码

在实际开发中,不仅要开发功能,更重要的是确保这些功能稳定可靠,并且拥有一个不错的性能,要确保这些就要对代码进行测试,开发人员通常会进行单元测试和性能测试。不同的语言通常都有自己的测试包/模块,Go 语言也一样,在 Go 中可以通过 testing 包对代码进行单元和性能测试,下面就来详细介绍。

  • 如何进行单元测试
  • 如何进行压力测试、性能测试
  • 如何进行性能分析

Go 语言测试支持

Go 语言有自带的测试框架testing,可以用来实现单元测试和性能测试,通过 go test 命令来执行单元测试和性能测试。

go test 执行测试用例时,是以go包为单位进行测试的。执行时需要制定包名,比如:go test 包名,如果没有指定包名,默认会选择执行命令时所在的包。go test 在执行时会遍历以 _test.go 结尾的源码文件,执行其中以 TestBenchmarkExample 开头的测试函数。其中源码文件需要满足以下规范:

  • 文件名必须是 _test.go 结尾,跟源文件在同一个包
  • 测试用例函数必须以Test、Benchmark、Example开头
  • 执行测试用例时的顺序,会按照源码中的顺序依次执行
  • 单元测试函数 TestXxx() 的参数是 testing.T,可以使用该类型来记录错误或测试状态
  • 性能测试函数 BenchmarkXxx() 的参数是 testing.B,函数内以 b.N 作为循环次数,其中 N 会动态变化
  • 示例函数 ExampleXxx() 没有参数,执行完会将输出与注释 // Output:进行对比
  • 测试函数原型:func TestXxx(t *testing.T),Xxx 部分为任意字母数字组合,首字母大写,例如: TestgenShortId 是错误的函数名,TestGenShortId 是正确的函数名
  • 通过调用 testing.T 的 Error、Errorf、FailNow、Fatal、FatalIf 方法来说明测试不通过,通过调用 Log、Logf 方法来记录测试信息:
t.Log t.Logf     # 正常信息 
t.Error t.Errorf # 测试失败信息 
t.Fatal t.Fatalf # 致命错误,测试程序退出的信息
t.Fail     # 当前测试标记为失败
t.Failed   # 查看失败标记
t.FailNow  # 标记失败,并终止当前测试函数的执行,需要注意的是,我们只能在运行测试函数的 Goroutine 中调用 t.FailNow 方法,而不能在我们在测试代码创建出的 Goroutine 中调用它
t.Skip     # 调用 t.Skip 方法相当于先后对 t.Log 和 t.SkipNow 方法进行调用,而调用 t.Skipf 方法则相当于先后对 t.Logf 和 t.SkipNow 方法进行调用。方法 t.Skipped 的结果值会告知我们当前的测试是否已被忽略
t.Parallel # 标记为可并行运算
// 被测试代码 util.go

package util

import (
    "github.com/teris-io/shortid"
)

func GenShortId() (string, error) {
    return shortid.Generate()
}

编写测试用例(对 GenShortId 函数进行单元测试)

util 目录下创建文件 util_test.go, 内容为:

package util

import (
    "testing"
)

func TestGenShortId(t *testing.T) {
    shortId, err := GenShortId()
    if shortId == "" || err != nil {
        t.Error("GenShortId failed!")
    }

    t.Log("GenShortId test pass")
}

$ go test -v -count 2 // 执行测试用例,2表示执行2次。

编写性能测试用例

util/util_test.go 测试文件中,新增两个性能测试函数: BenchmarkGenShortId()BenchmarkGenShortIdTimeConsuming()

func BenchmarkGenShortId(b *testing.B) {
    for i := 0; i < b.N; i++ {
        GenShortId()
    }
}

func BenchmarkGenShortIdTimeConsuming(b *testing.B) {
    b.StopTimer() //调用该函数停止压力测试的时间计数

    shortId, err := GenShortId()
    if shortId == "" || err != nil {
        b.Error(err)
    }

    b.StartTimer() //重新开始时间

    for i := 0; i < b.N; i++ {
        GenShortId()
    }
}

说明

  • 性能测试函数名必须以 Benchmark 开头,如 BenchmarkXxx 或者 Benchmark_xxx
  • go test 默认不会执行压力测试函数,需要通过指定参数 -test.bench 来运行压力测试函数, -test.bench 后跟正则表达式,如 go test -test.bench=".*" 表示执行所有的压力测试函数
  • 在压力测试中,需要在循环体中指定 test.B.N 来循环执行压力测试代码

执行压力测试

  • util 目录下执行命令 go test -test.bench=".*"
➜  apiserver/util master ✗ go test -test.bench=".*"
goos: darwin
goarch: amd64
pkg: apiserver/util
BenchmarkGenShortId-4                    1000000              1660 ns/op
BenchmarkGenShortIdTimeConsuming-4       1000000              1646 ns/op
PASS
ok      apiserver/util  3.361s
  • 上面命令只执行压力测试函数,不执行单元测试函数
  • 第一条显示 BenchmarkGenShortId 执行了100000次,每次的平均时间是1660纳秒
  • 第二条显示 BenchGenShortIdTimeConsuming 执行了100000次,每次的平均执行时间是1646纳秒
  • 最后一条显示中的执行时间

BenchmarkGenShortIdTimeConsumingBenchmarkGenShortId多了两个调用 b.StopTimer()b.startTimer()

  • b.StopTimer():调用该函数停止压力测试的时间计数
  • b.startTimer():重新开始时间

b.StopTimer()b.StartTimer() 之间可以做一些准备工作,这样这些时间不影响我们测试函数本身的性能。

查看性能并生产函数调用图

  • 执行如下命令,会在当前目录下生成 cpu.profileutil.test 文件。
$ go test -bench=".*" -cpuprofile=cpu.profile ./util
  • 执行 go tool pprof util.test cpu.profile 查看性能(进入交互页面后执行 top 指令)
$ go tool pprof util.test cpu.profile 
File: util.test
Type: cpu
Time: Jul 9, 2018 at 1:41pm (CST)
Duration: 3.57s, Total samples = 3.46s (96.84%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 3.22s, 93.06% of 3.46s total
Dropped 22 nodes (cum <= 0.02s)
Showing top 10 nodes out of 73
      flat  flat%   sum%        cum   cum%
     2.62s 75.72% 75.72%      2.62s 75.72%  syscall.Syscall /usr/local/go/src/syscall/asm_darwin_amd64.s
     0.30s  8.67% 84.39%      0.30s  8.67%  runtime.kevent /usr/local/go/src/runtime/sys_darwin_amd64.s
     0.07s  2.02% 86.42%      0.28s  8.09%  runtime.mallocgc /usr/local/go/src/runtime/malloc.go
     0.05s  1.45% 87.86%      0.05s  1.45%  runtime.mach_semaphore_signal /usr/local/go/src/runtime/sys_darwin_amd64.s
     0.05s  1.45% 89.31%      0.05s  1.45%  runtime.mach_semaphore_timedwait /usr/local/go/src/runtime/sys_darwin_amd64.s
     0.03s  0.87% 90.17%      2.84s 82.08%  github.com/teris-io/shortid.maskedRandomInts /Users/liuzhiwang/git-lzw/go-demo/src/github.com/teris-io/shortid/shortid.go
     0.03s  0.87% 91.04%      0.05s  1.45%  runtime.freedefer /usr/local/go/src/runtime/panic.go
     0.03s  0.87% 91.91%      0.10s  2.89%  runtime.slicerunetostring /usr/local/go/src/runtime/string.go
     0.02s  0.58% 92.49%      3.02s 87.28%  github.com/teris-io/shortid.(*Abc).Encode /Users/liuzhiwang/git-lzw/go-demo/src/github.com/teris-io/shortid/shortid.go
     0.02s  0.58% 93.06%      2.74s 79.19%  io.ReadFull /usr/local/go/src/io/io.go
(pprof) 

pprof程序中最重要的命令就是 topN,此命令用于显示profile文件中的最靠前的N个样本(sample),它的输出格式各字段的含义依次是:

  • 采样点落在该函数中的总时间
  • 采样点落在该函数中的百分比
  • 上一项的累积百分比
  • 采样点落在该函数,以及被它调用的函数中的总时间
  • 采样点落在该函数,以及被它调用的函数中的总次数百分比
  • 函数名

此外,在pprof程序中还可以使用 svg 命令来生成函数调用关系图(需要安装graphviz),例如:

关于如何看懂pprof信息,请参考 Profiling Go Programs

关于如何做性能分析,参考郝林大神的文章 go tool pprof

测试覆盖率

  • 我们写单元测试的时候应该想得很全面,能够覆盖到所有的测试用例,但有时也会漏过一些 case,go 提供了 cover 工具来统计测试覆盖率。

  • go test -coverprofile=cover.out :在测试文件目录下运行测试并统计测试覆盖率

  • go tool cover -func=cover.out :分析覆盖率文件,可以看出哪些函数没有测试,哪些函数内部的分支没有测试完全,cover 工具会通过执行代码的行数与总行数的比例表示出覆盖率

$ go test -coverprofile=cover.out
PASS
coverage: 14.3% of statements
ok      apiserver/util  0.006s

$ go tool cover -func=cover.out
apiserver/util/util.go:8:   GenShortId  100.0%
apiserver/util/util.go:12:  GetReqID    0.0%
total:              (statements)    14.3%

本文链接:参与评论 »

--EOF--

提醒:本文最后更新于 154 天前,文中所描述的信息可能已发生改变,请谨慎使用。

专题「跟我一起学Go」的其它文章 »

Comments