11-测试&性能

单元测试

go test工具

==高亮==

Go语言中的测试依赖 go test 命令,编写测试代码和编写普通的Go代码过程类似,只是需要遵循一定的约定和特定要求。

  • 所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中
  • *_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型格式作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档
  • go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

测试函数

格式

每个测试函数必须导入testing包,基本格式:

func TestName(t *testing.T){
    // ...
}
  • 测试函数的名字必须以Test开头
  • 可选的后缀名必须以大写字母开头
  • 参数必须是*testing.T
    func TestAdd(t *testing.T){ ... }
    func TestSum(t *testing.T){ ... }
    func TestLog(t *testing.T){ ... }
    • 其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下
    func (c *T) Error(args ...interface{})
    func (c *T) Errorf(format string, args ...interface{})
    func (c *T) Fail()
    func (c *T) FailNow()
    func (c *T) Failed() bool
    func (c *T) Fatal(args ...interface{})
    func (c *T) Fatalf(format string, args ...interface{})
    func (c *T) Log(args ...interface{})
    func (c *T) Logf(format string, args ...interface{})
    func (c *T) Name() string
    func (t *T) Parallel()
    func (t *T) Run(name string, f func(t *T)) bool
    func (c *T) Skip(args ...interface{})
    func (c *T) SkipNow()
    func (c *T) Skipf(format string, args ...interface{})
    func (c *T) Skipped() bool

示例

示例测试函数

package splitString

import "strings"

//切割字符串,用sep切割str

func Split(str string,sep string) (result []string) {
    index := strings.Index(str,sep) //拿到sep在str中的索引
    for index > -1 { //只要能拿到索引,也就是包含这个字符
        result = append(result,str[:index])  //把str中index前一部分追加到要返回的result中
        //str = str[index+1:]             //让str变成index的后半部分
        str = str[index+len(sep):]  //解决多个字符串访问
        index = strings.Index(str,sep)    //继续得到index
    }
    result = append(result,str) //最后剩下的一部分追加进去
    return
}

测试用例

//splitString_test.go
package splitString

import (
    "reflect"
    "testing"
)


func TestSplit(t *testing.T) {
    got :=Split("a:b:c",":")   // 程序输出的结果
    want := []string{"a", "b", "c"}      // 期望的结果
    if !reflect.DeepEqual(want, got) { // 因为slice是引用类型不能比较直接,借助反射包中的方法比较
        t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
    }
}

func Test2Split(t *testing.T) {
    got :=Split("a,b,c",",")
    want := []string{"a", "b", "c"}
    if !reflect.DeepEqual(want, got) {
        t.Errorf("excepted:%v, got:%v", want, got)
    }
}

func TestMoreSplit(t *testing.T) {
    got :=Split("abcd","bc")
    want := []string{"a", "d"}
    if !reflect.DeepEqual(want, got) {
        t.Fatalf("excepted:%v, got:%v", want, got)
        //#号能看到切片中的空串
    }
}
  • 在当前包路径下,执行go test命令,可以看到输出结果
    • 添加-v参数,可以查看测试函数名称和运行时间
    • 添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被

测试组

写很多个测试函数很麻烦,可以定义测试组,一个个执行

  • 定义一个测试用例的结构体
    • 包含测试需要的参数和预期的结果
  • 定义一个存储测试用例的切片
  • 遍历切片,逐一执行测试用例
func TestSplit(t *testing.T) {
    //定义一个测试用例类型
    type testCase struct {
        input string
        sep   string
        want  []string
    }

    // 定义一个存储测试用例的切片
    tests := []testCase{
        {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        {input: "怒老龙恼怒闹老农", sep: "老", want: []string{"怒", "龙恼怒闹","农"}},
    }

    // 遍历切片,逐一执行测试用例
    for _, tc := range tests {
        got := Split(tc.input, tc.sep)
        if !reflect.DeepEqual(got, tc.want) {
            t.Errorf("excepted:%v, got:%v", tc.want, got)
        }
    }
}

子测试

如果测试用例比较多的时候,没办法一眼看出来具体是哪个测试用例失败了

func TestSplit(t *testing.T) {
    type test struct { // 定义test结构体
        input string
        sep   string
        want  []string
    }
    tests := map[string]test{ // 测试用例使用map存储
        "simple":      {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
        "wrong sep":   {input: "a:b:c", sep: ",", want: []string{"a:b:c"}},
        "more sep":    {input: "abcd", sep: "bc", want: []string{"a", "d"}},
        "leading sep": {input: "怒老龙恼怒闹老农", sep: "龙", want: []string{"怒", "龙恼怒闹","农"}},//错误
    }
    for name, tc := range tests {
        t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试
            got := Split(tc.input, tc.sep)
            if !reflect.DeepEqual(got, tc.want) {
                t.Errorf("excepted:%#v, got:%#v", tc.want, got)
            }
        })
    }
}

此时能看到更清晰的错误输出

  • 通过-run=RegExp来指定运行的测试用例
  • 通过/来指定要运行的子测试用例,例如:go test -v -run=Split/simple只会运行simple对应的子测试用例。

覆盖率测试

  • 查看单元测试覆盖代码的百分比
  • 使用go test -cover来查看测试覆盖率
    split $ go test -cover
    输出:
    PASS
    coverage: 100.0% of statements  //覆盖率
    ok    studygo/code_demo/test_demo/split       0.005s
    
  • 还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件
    split $ go test -cover -coverprofile=c.out
    PASS
    coverage: 100.0% of statements
    ok      github.com/Q1mi/studygo/code_demo/test_demo/split       0.005s
    
    • 执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个HTML报告


基准测试

(性能)基准测试就是在一定的工作负载之下检测程序性能

格式

  • 基准测试以Benchmark为前缀,后缀是要测试的函数名,不是函数所在文件名
  • 需要一个*testing.B类型的参数b
  • 基准测试必须要执行b.N次,这样的测试才有对照性
    • b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。
func BenchmarkName(b *testing.B){
    // ...
}

示例

还是之前的函数,把测试函数写在之前的测试文件中(就是_test结尾的文件中)

func BenchmarkSplit(b *testing.B)  {
    for i:= 0; i<b.N;i++{
        Split("你是那个那个那个那个谁","那")
    }
}

  • BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。
  • 10000000276ns/op表示每次调用Split函数耗时203ns,这个结果是10000000次调用的平均值

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

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

试着提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加,此时内存分配次数会大大减少

性能比较函数

上面的基准测试只能得到给定操作的绝对耗时,但是在很多性能问题是发生在两个不同操作之间的相对耗时,比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少?再或者对于同一个任务究竟使用哪种算法性能最佳?我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。

func benchmark(b *testing.B, size int){/* ... */}
func Benchmark10(b *testing.B){ benchmark(b, 10) }
func Benchmark100(b *testing.B){ benchmark(b, 100) }
func Benchmark1000(b *testing.B){ benchmark(b, 1000) }

例如:

func benchmarkFib(b *testing.B, n int) {
    for i := 0; i < b.N; i++ {
        Fib(n)
    }
}

func BenchmarkFib1(b *testing.B)  { benchmarkFib(b, 1) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(b, 2) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(b, 3) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) }

了解更多

性能调优

Go语言项目中的性能优化主要有以下几个方面:

  • CPU profile:报告程序的 CPU 使用情况,按照一定频率去采集应用程序在 CPU 和寄存器上面的数据
  • Memory Profile(Heap Profile):报告程序的内存使用情况
  • Block Profiling:报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
  • Goroutine Profiling:报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的

采集性能数据

Go语言内置了获取程序的运行数据的工具,包括以下两个标准库:

  • runtime/pprof:采集工具型应用运行数据进行分析
  • net/http/pprof:采集服务型应用运行时数据进行分析

pprof开启后,每隔一段时间(10ms)就会收集下当前的堆栈信息,获取格格函数占用的CPU以及内存资源;最后通过对这些采样数据进行分析,形成一个性能分析报告。

注意,我们只应该在性能测试的时候才在代码中引入pprof

工具类应用

运行一段时间就会退出的应用

使用runtime/pprof库,在应用退出的时候把 profiling 的报告保存到文件中,进行分析

import "runtime/pprof"

cpu性能分析

开启

pprof.StartCPUProfile(w io.Writer)

停止

pprof.StopCPUProfile()

应用执行结束后,就会生成一个文件,保存了我们的 CPU profiling 数据。得到采样数据之后,使用go tool pprof工具进行CPU性能分析。

内存性能分析

记录程序的堆栈信息

pprof.WriteHeapProfile(w io.Writer)

得到采样数据之后,使用go tool pprof工具进行内存性能分析。

go tool pprof默认是使用-inuse_space进行统计,还可以使用-inuse-objects查看分配对象的数量。

服务类应用

应用程序是一直运行的,比如 web 应用,可以使用net/http/pprof库提供 HTTP 服务进行分析

  • 若使用默认http.DefaultServeMux(通常是代码直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil)),只需要在你的web server端代码中按如下方式导入net/http/pprof
import _ "net/http/pprof"
  • 如果你使用自定义的 Mux,则需要手动注册一些路由规则:
    r.HandleFunc("/debug/pprof/", pprof.Index)
    r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
    r.HandleFunc("/debug/pprof/profile", pprof.Profile)
    r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
    r.HandleFunc("/debug/pprof/trace", pprof.Trace)
  • 如果你使用的是gin框架,那么推荐使用"github.com/DeanThompson/ginpprof"

不管哪种方式,HTTP 服务都会多出/debug/pprof,访问它会得到类似下面的内容:

  • /debug/pprof/profile:访问这个链接会自动进行 CPU profiling,持续 30s,并生成一个文件供下载
  • /debug/pprof/heap: Memory Profiling 的路径,访问这个链接会得到一个内存 Profiling 结果的文件
  • /debug/pprof/block:block Profiling 的路径
  • /debug/pprof/goroutines:运行的 goroutines 列表,以及调用关系

go tool pprof

使用 go tool pprof 工具对获取的性能数据进行分析,基本的使用方式:

go tool pprof [binary] [source]
  • binary 是应用的二进制文件,用来解析各种符号;
  • source 表示 profile 数据的来源,可以是本地的文件,也可以是 http 地址。

性能分析案例

用一段有性能问题的代码作为测试案例

// 一段有性能问题的代码
func logicCode() {
    var c chan int
    for {
        select {
        case v := <-c:    //阻塞
            fmt.Printf("recv from chan, value:%v\n", v)
        default:
            time.Sleep(time.Millisecond*500)  //让出CPU
        }
    }
}

func main() {
    var isCPUPprof bool //是否开启CPU profile的标志位
    var isMemPprof bool //是否开启内存profile的标志位

    flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on")
    flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on")
    flag.Parse()

    if isCPUPprof {
        file, err := os.Create("./cpu.pprof")  //当前路径创建cpu.pprof文件
        if err != nil {
            fmt.Printf("create cpu pprof failed, err:%v\n", err)
            return
        }
        pprof.StartCPUProfile(file)   //开始记录CPU信息
        defer pprof.StopCPUProfile()      //注册延迟停止
    }

    if isMemPprof {
        file, err := os.Create("./mem.pprof")
        if err != nil {
            fmt.Printf("create mem pprof failed, err:%v\n", err)
            return
        }
        pprof.WriteHeapProfile(file)
        file.Close()
    }

    for i := 0; i < 8; i++ {
        go logicCode()
    }

    time.Sleep(20 * time.Second)

}
  • 通过flag 在命令行运行时加上 -cpu=true,三十秒后生成cpu.pprof文件

命令行交互界面

  • 使用 pprof 工具分析 .pprof 文件
    go tool pprof cpu.pprof

  • (pprof) 后面可以输入一些命令来查看分析信息
    • 输入top3来查看程序中占用CPU前3位的函数
    • flat:当前函数占用CPU的耗时
    • flat::当前函数占用CPU的耗时百分比
    • sun%:函数占用CPU的耗时累计百分比
    • cum:当前函数加上调用当前函数的函数占用CPU的总耗时
    • cum%:当前函数加上调用当前函数的函数占用CPU的总耗时百分比
    • 最后一列:函数名称
    • 使用 list 函数名 命令查看具体的函数分析

图形化查看

可以直接输入web,通过svg图的方式查看程序中详细的CPU占用情况。

  • 想要查看图形化的界面首先需要安装graphviz图形化工具
  • Windows: 下载graphvizgraphviz安装目录下的bin文件夹添加到Path环境变量中。 在终端输入dot -version查看是否安装成功

go-torch 火焰图

火焰图(Flame Graph)是 Bredan Gregg 创建的一种性能分析图表,因为它的样子近似 🔥而得名。

go-torch是 uber 开源的一个工具,可以直接读取 golang profiling 数据,并生成一个火焰图的 svg 文件。

安装go-torch

  go get -v github.com/uber/go-torch
  • svg 文件可以通过浏览器打开,它对于调用图的最优点是它是动态的:可以通过点击每个方块来 zoom in 分析它上面的内容。
  • 火焰图的调用顺序从下到上,每个方块代表一个函数,它上面一层表示这个函数会调用哪些函数,方块的大小代表了占用 CPU 使用的长短。火焰图的配色并没有特殊的意义,默认的红、黄配色是为了更像火焰而已
  • 没有任何参数的话,它会尝试从http://localhost:8080/debug/pprof/profile获取 profiling 数据。它有三个常用的参数可以调整:
    • -u –url:要访问的 URL,这里只是主机和端口部分
    • -s –suffix:pprof profile 的路径,默认为 /debug/pprof/profile
    • –seconds:要执行 profiling 的时间长度,默认为 30s

安装FlameGraph

要生成火焰图,需要事先安装 FlameGraph工具

  • 下载安装perl:https://www.perl.org/get.html
  • 下载FlameGraph:git clone https://github.com/brendangregg/FlameGraph.git
  • FlameGraph目录加入到操作系统的环境变量中。
  • Windows平台的同学,需要把go-torch/render/flamegraph.go文件中的GenerateFlameGraph按如下方式修改,然后在go-torch目录下执行go install即可。
// GenerateFlameGraph runs the flamegraph script to generate a flame graph SVG. func GenerateFlameGraph(graphInput []byte, args ...string) ([]byte, error) {
flameGraph := findInPath(flameGraphScripts)
if flameGraph == "" {
    return nil, errNoPerlScript
}
if runtime.GOOS == "windows" {
    return runScript("perl", append([]string{flameGraph}, args...), graphInput)
}
  return runScript(flameGraph, args, graphInput)
}

wrk压力测试工具

推荐使用https://github.com/wg/wrkhttps://github.com/adjust/go-wrk

使用go-torch

  • 使用wrk进行压测:go-wrk -n 50000 http://127.0.0.1:8080/book/list
  • 在上面压测进行的同时,打开另一个终端执行go-torch -u http://127.0.0.1:8080 -t 30,30秒之后终端会出现如下提示:Writing svg to torch.svg
  • 然后我们使用浏览器打开torch.svg就能看到如下火焰图了。

火焰图的y轴表示cpu调用方法的先后,x轴表示在每个采样调用时间内,方法所占的时间百分比,越宽代表占据cpu时间越多。通过火焰图我们就可以更清楚的找出耗时长的函数调用,然后不断的修正代码,重新采样,不断优化。

pprof与性能测试结合

go test命令有两个参数和 pprof 相关,它们分别指定生成的 CPU 和 Memory profiling 保存的文件:

  • -cpuprofile:cpu profiling 数据要保存的文件地址
  • -memprofile:memory profiling 数据要报文的文件地址

还可以选择将pprof与性能测试相结合,比如:

  • 下面执行测试的同时,也会执行 CPU profiling,并把结果保存在 cpu.prof 文件中:
    go test -bench . -cpuprofile=cpu.prof
  • 下面执行测试的同时,也会执行 Mem profiling,并把结果保存在 cpu.prof 文件中:
    go test -bench . -memprofile=./mem.prof

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">

posted @ 2020-07-09 08:57  ZackYJ  阅读(78)  评论(0)    收藏  举报