go test命令

go test命令只运行单元测试,添加-bench=.参数,go test同时执行单元测试和基准测试,当然可以通过正则过滤只运行基准测试,例如-bench=^Benchmark。添加-conver参数展示测试覆盖率。


测试代码

测试5种字符串拼接效果,共两个文件splice.go和splice_test.go

splice.go文件内容如下:

package splice

import (
    "bytes"
    "fmt"
    "strings"
)

// SpliceWithPlus 使用+号拼接字符串
func SpliceWithPlus(s1 string, s2 string) string {
    return s1 + s2
}

// SpliceWithSprintf 使用fmt.Sprintf拼接字符串
func SpliceWithSprintf(s1 string, s2 string) string {
    return fmt.Sprintf("%s%s", s1, s2)
}

// SpliceWithJoin 使用strings.Join拼接字符串
func SpliceWithJoin(s1 string, s2 string) string {
    return strings.Join([]string{s1, s2}, "")
}

// SpliceWithBuilder 使用strings.Builder拼接字符串
func SpliceWithBuilder(s1 string, s2 string) string {
    var builder strings.Builder
    builder.WriteString(s1)
    builder.WriteString(s2)
    return builder.String()
}

// SpliceWithBuilder 使用bytes.Buffer拼接字符串
func SpliceWithBuffer(s1 string, s2 string) string {
    var bf bytes.Buffer
    bf.WriteString(s1)
    bf.WriteString(s2)
    return bf.String()
}


测试文件splice_test.go内容如下:

package test_example

import "testing"

var (  
    count = 10
    s1    = strings.Repeat("1234567890", count)
    s2    = strings.Repeat("0987654321", count)
    want  = s1 + s2
)

func TestSpliceString(t *testing.T) {
    if got := SpliceWithPlus(s1, s2); got != want {
        t.Errorf("SpliceWithPlus() = %v, want %v", got, want)
    }
    if got := SpliceWithSprintf(s1, s2); got != want {
        t.Errorf("SpliceWithSprintf() = %v, want %v", got, want)
    }
    if got := SpliceWithJoin(s1, s2); got != want {
        t.Errorf("SpliceWithJoin() = %v, want %v", got, want)
    }
    if got := SpliceWithBuilder(s1, s2); got != want {
        t.Errorf("SpliceWithBuilder() = %v, want %v", got, want)
    }
    if got := SpliceWithBuffer(s1, s2); got != want {
        t.Errorf("SpliceWithBuffer() = %v, want %v", got, want)
    }
}

func BenchmarkSpliceWithPlus(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SpliceWithPlus(s1, s2)
    }
}

func BenchmarkSpliceWithSprintf(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SpliceWithSprintf(s1, s2)
    }
}

func BenchmarkSpliceWithJoin(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SpliceWithJoin(s1, s2)
    }
}

func BenchmarkSpliceWithBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SpliceWithBuilder(s1, s2)
    }
}

func BenchmarkSpliceWithBuffer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SpliceWithBuffer(s1, s2)
    }
}


单元测试

单元测试常用参数:

  • -v:测试时显示详细信息,示例:go test -v
  • -list: 列出测试、基准测试函数,示例:go test -list
  • -run: 指定要运行的测试函数,示例:go test -run TestSpliceString
  • -failfast: 在第一个失败的测试处停止,退出测试,示例 go test -failfast
  • -count=1: 禁用缓存,示例:go test -count=1
  • -cpu=4: 限制cpu数量,把参数值传递给GOMAXPROCS,示例:go test -cpu=4
  • -race: 检测竞态条件,示例:go test -race


单元测试范围示例:

# 执行当前下所有单元测试,包括子文件夹
go test ./...

# 执行当前目录下所有单元测试,不包括子文件夹
go test ./dir

# 执行指定文件单元测试,指定测试文件和被测试文件
go test splice.go splice_test.go

# 运行指定单元测试用例
go test -run TestSpliceString


基准测试

基准测试框架对一个测试用例的默认测试时间是 1 秒。开始测试时,当以 Benchmark 开头的基准测试用例函数返回时还不到 1 秒,那么 testing.B 中的 N 值将按 1、2、5、10、20、50……递增,同时以递增后的值重新调用基准测试用例函数。

基准测试常用参数:

  • -bench: 基准测试必填参数,参数值是正则表达式, -bench=. 表示同时运行单元测试和基准测试; -bench=^Benchmark 表示执行当前目录下所有基准测试,也可以具体到某个函数的基准测试。
  • benchtime=3s: 自定义测试时间,示例:go test -bench=. -benchtime=3s
  • -benchmem: 显示内存分配统计信息,示例:go test -bench=. -benchmem
  • -cpu=4: 限制线程数量,把参数值传递给GOMAXPROCS,示例:go test -bench=. -cpu=4

基准测试范围示例:

# 执行当前目录下所有基准测试,包括子文件夹
go test -bench=. ./...

# 执行当前目录下的基准测试,不包括子文件夹
go test -bench=. ./dir

# 执行指定文件基准测试,指定测试文件和被测试文件
go test -bench=. splice.go splice_test.go

# 运行指定单元基准测试
go test -bench=BenchmarkSpliceWithPlus

修改splice_test.go文件的count变量值,分别为1、10、100,进行基准测试,结果如下:

go version go1.17.2 windows/amd64

# count=1,10bytes 长度字符串拼接基准压测
$ go test -bench=. -benchmem

goos: windows
goarch: amd64
pkg: demo/test_example
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSpliceWithPlus-12              80041087                14.95 ns/op            0 B/op          0 allocs/op
BenchmarkSpliceWithSprintf-12            8744901               128.1 ns/op            56 B/op          3 allocs/op
BenchmarkSpliceWithJoin-12              33571974                35.15 ns/op           24 B/op          1 allocs/op
BenchmarkSpliceWithBuilder-12           20154652                59.78 ns/op           48 B/op          2 allocs/op
BenchmarkSpliceWithBuffer-12            19531503                62.91 ns/op           88 B/op          2 allocs/op
PASS
ok      demo/test_example       6.314s


# count=10,100bytes 长度字符串拼接基准压测
$ go test -bench=. -benchmem

goos: windows
goarch: amd64
pkg: demo/test_example
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSpliceWithPlus-12              15102146                66.62 ns/op          208 B/op          1 allocs/op
BenchmarkSpliceWithSprintf-12            6655336               181.0 ns/op           240 B/op          3 allocs/op
BenchmarkSpliceWithJoin-12              17094942                70.57 ns/op          208 B/op          1 allocs/op
BenchmarkSpliceWithBuilder-12           10660695               112.0 ns/op           336 B/op          2 allocs/op
BenchmarkSpliceWithBuffer-12             5528898               201.4 ns/op           640 B/op          3 allocs/op
PASS
ok      demo/test_example       6.610s


# count=100,1000bytes 长度字符串拼接基准压测
$ go test -bench=. -benchmem

goos: windows
goarch: amd64
pkg: demo/test_example
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
BenchmarkSpliceWithPlus-12               2898604               387.1 ns/op          2048 B/op          1 allocs/op
BenchmarkSpliceWithSprintf-12            2218687               511.3 ns/op          2081 B/op          3 allocs/op
BenchmarkSpliceWithJoin-12               2922162               412.0 ns/op          2048 B/op          1 allocs/op
BenchmarkSpliceWithBuilder-12            2015064               607.7 ns/op          3072 B/op          2 allocs/op
BenchmarkSpliceWithBuffer-12              999949                1186 ns/op          6144 B/op          3 allocs/op
PASS
ok      demo/test_example       8.067s

基准压测函数后面数字12,表示执行基准测试使用的线程数量,可以通过-cpu=4参数来修改。

5种字符串拼接性能最好的是BenchmarkSpliceWithPlus,其次是BenchmarkSpliceWithJoin。


Benchstat工具

单纯只压测一次结果不大准确,因此使用工具Benchstat 计算和比较有关基准的统计数据。

安装:

go install golang.org/x/perf/cmd/benchstat@latest

用法:

benchstat [-delta-test name] [-geomean] [-html] [-sort order] old.txt [new.txt] [more.txt …]

示例:

# 第一次压测某个函数
go test -run=NONE -bench=BenchmarkSpliceWithSprintf -count=5 | tee -a old.txt

# 优化后(使用+号替换fmt.Sprintf拼接字符串)第二次压测某个函数
go test -run=NONE -bench=BenchmarkSpliceWithSprintf -count=5 | tee -a new.txt

# 查看压测结果
benchstat old.txt
benchstat new.txt

# 比较两次的统计结果
benchstat old.txt new.txt
# name                  old time/op  new time/op  delta
# SpliceWithSprintf-12   128ns ± 1%    33ns ± 0%  -74.63%  (p=0.000 n=14+5)

任何大于 5% 的值都表明某些样本不可靠,在这种情况下,您应该重新运行基准测试,尽可能保持环境稳定以提高可靠性。


测试覆盖率

go test命令还支持展示测试覆盖率信息。

测试覆盖率常用参数:

  • -cover: 测试覆盖率必填参数,示例:go test -v -cover
  • -coverprofile: 导出单元测试覆盖率统计信息到文件,示例:go test -coverprofile=cover.out
  • -html: 在浏览器查看覆盖哪些代码,示例:go tool cover -html=cover.out

测试覆盖率范围示例:

# 执行当前下所有单元测试,包括子文件夹,并展示覆盖率
go test -cover ./...

# 执行当前目录下所有单元测试,不包括子文件夹,并展示覆盖率
go test -cover ./dir

# 执行指定文件单元测试,指定测试文件和被测试文件,并展示覆盖率
go test -cover splice.go splice_test.go

# 运行指定单元测试用例,并展示覆盖率
go test -cover -run TestSpliceString
# 在浏览器查看覆盖哪些代码
go test -coverprofile=cover.out && go tool cover -html=cover.out


总结

单元测试是项目开发环节必不可少的环节,是检验程序是否符合预期手段,测试代码有可能比实际业务逻辑代码还多,主要原因是测试包括各种测试case,这些case尽可能覆盖率所有业务代码,使用goland IDE写代码,按快捷键Alt+Insert可以自动化添加单元测试代码,只需要填写测试用例,大大减少写测试代码时间。基准测试是性能要求比较高的业务逻辑中必须测试环节,通过暴力压测方式检验程序性能达到什么水平,一般看完成操作耗时和内存分配情况来判断,也是程序性能调优的常用手段。



专题「golang相关」的其它文章 »