测试

1. 单元测试
2. 性能测试
3. 代码覆盖率
4. 性能监控

单元测试

单元测试是开发人员的一项基本工作,Go语言提供的单元测试机制不仅可以测试代码的逻辑算法是否准确, 并且可以监控代码的质量。在任何时候都可以使用简单的命令验证全部功能,找出未完成的任务和因为中途代码修改导致的错误,它与性能测试以及 代码覆盖率等一起保障了代码总是在可控范围内。单元测试伴随在整个项目开发过程中,所以说单元测试代码同主工程一样重要。

Go语言工具链和标准库提供了单元测试框架,给开发人员带来很大遍历,但也要按照如下需求编写测试代码,比如:

- 测试代码要以*_test.go文件的形式放在当前包
- 测试函数要以*Test形式命名
- go test xxx_test.go 测试命令将忽略以"\_"和 "."开头的测试文件
- go install/build 正常编译操作会忽略测试文件
import "testing"
func sum(x,y int)int{
    return x+y 
}
func TestSum(t *testing.T){                                                               
    if sum(1,2) !=3 {
        t.FailNow()
    }   
}
测试:
#go test -v  //测试当前包和所有子包,用go test -v ./...

testing.T 提供了控制测试结果和行为的方法,常用的如下,比如:

Fail: 失败后继续执行当前测试函数
FailNow: 失败后立即终止执行当前测试函数
SkipNow: 跳过执行当前测试函数
Log: 报告错误信息,仅当失败或-v时输出
Parallel: 与有同样设置的测试函数并行执行
Error: 报告错误信息后继续,相当于Log + Fail
Fatal: 报告错误信息后停止,相当于FailNow + Log

可以利用t.Parallel()函数并行执行多个test case,提高测试效率,比如:

func TestA(t *testing.T){
    t.Parallel()
    time.Sleep(time.Second *2)
}

func TestB(t *testing.T){
    t.Parallel()
    time.Sleep(time.Second *2)
}
  • TableDrivenTests

如果在测试过程中发现自己使用复制和粘贴来做重复的功能测试,那么可以考虑使用类似数据表的模式来批量输入 条件进行测试,每个表项是一个完整的测试用例的输入和预期结果,比如:

func sum(x,y int)int{
    return x+y 
}
func TestSum(t *testing.T){
    var tests = []struct{
        x, y , expect int 
    }{  
        {1,2,3},
        {1,3,3},
        {1,4,5},
    }   
    for _, u := range tests{
        result := sum(u.x, u.y)
        if result != u.expect {
            t.Errorf("%d + %d expect %d, actual result: %d\n", u.x, u.y, u.expect, result)                 
        }   
    }   
}
  • TestMain 有时候需要为测试用例提供初始化和清理工作,但是testing并没有提供setup/teardown的机制,所以 可以利用TestMain函数来增加测试用例的测试入口,实现控制测试用例的调用方式,比如:
func sum(x,y int)int{
    return x+y 
}
                                                                                                           
func TestSum(t *testing.T){
    println("TSum")
    if sum(1,2) !=3 {
        t.FailNow()
    }
}

func TestMain(m *testing.M){
    println("setup")
    m.Run()
    println("teardown")
    os.Exit(0)
}

可以借助MainStart自行构建M对象,实现多测试用例的组合,比如:

func MainStart(matchString func(pat, str string) (bool, error), tests []InternalTest, benchmarks []In    ternalBenchmark, examples []InternalExample) *M
func TestMain(m *testing.M){
    match := func(pat, str string)(bool,error){
        matchRe, err := regexp.Compile(pat)
        if err != nil{
            return false, err 
        }   
        return matchRe.MatchString(str), nil 
    }   

    tests := []testing.InternalTest{
        {"sum",TestSum},
        {"a",TestA},                                                                                       
        {"b",TestB},
    }   

    benchmarks := []testing.InternalBenchmark{}
    examples := []testing.InternalExample{}
    m = testing.MainStart(match, tests,benchmarks, examples)
    os.Exit(m.Run())
}
执行:
go test -v sum3_test.go -run "a"

常用测试参数: | 参数 | 说明 | 示例 | |:—:|:—:|:—:| | -args | go test 测试命令行参数 |-args “a” | | -v | 输出测试详细信息 | | | -parallel | 并行测试,默认为GOMAXPROCS| -parallel 4| | -run | 指定测试函数,支持正则表达式| -run “Sum” | | -timeout | 指定全部测试完成超时时间,默认是 10m | -timeout 3m10s | | -count | 指定重复测试次数| -count 3m10s |

性能测试

性能测试函数是以Benchmark为名称前缀的函数同样是保存在*_test.go文件中, 执行性能测试时,go test 必须使用-bench选项,它通过逐步调整B.N值,反复执行 测试函数,直到获得准确的测试结果,比如:

func sum(x ,y int)int{
    return x+y 
}
func BenchmarkSum(b *testing.B){
    println()
    for i:= 0; i < b.N;i++{
        sum(1,1)
    }   
}
输出:
#go test -v sum_test.go -bench .
BenchmarkSum-2
b.N:  1
b.N:  100
b.N:  10000
b.N:  1000000
b.N:  100000000
b.N:  2000000000
2000000000	         0.34 ns/op
PASS
ok  	command-line-arguments	0.732s
如果仅想进行性能测试, 可用-run NONE选项来忽略所有单元测试用例

在测试某些比较耗时代码场景下,默认的循环次数就会过少,这种情况取的平均值可能就不足以 准确计算性能,此时可以用benchtime选项来设定最小测试时间,增加循环次数,进而返回更准确的结果,比如:

func mysleep(){
    time.Sleep(time.Second)
}
func BenchmarkMysleep(b *testing.B){
    for i:= 0; i < b.N;i++{                                                     
        mysleep()
    }   
}
输出:
# go test -v sum5_test.go -bench .  -benchtime 10s
BenchmarkMysleep-2   	      10	1000629167 ns/op
PASS
ok  	command-line-arguments	11.013s

如果在测试过程中需要执行其它一些业务逻辑,而不想因这些逻辑影响测试结果,那么可以临时阻止 计时器工作,比如:

func sum(x ,y int)int{
    return x+y 
}
func BenchmarkSum(b *testing.B){
    time.Sleep(time.Second)
    b.ResetTimer()          //重置计时器
    for i:= 0; i < b.N;i++{
        if i == 100{
            b.StopTimer()    //暂停                                                   
            //do other
            time.Sleep(time.Second)
            //done
            b.StartTimer()  //恢复
        }   
        sum(1,1)
    }   
}
输出:
暂停计时器的效果:
BenchmarkSum-2   	2000000000	         0.72 ns/op
PASS
ok  	command-line-arguments	11.570s
不暂停计时器的效果:
BenchmarkSum-2   	   10000	    100053 ns/op
PASS
ok  	command-line-arguments	4.010s

性能测试过程中,除了关注目标代码的执行时间,还应该关注代码在堆上的内存分配,因为 内存分配与垃圾回收的相关操作也应该计入消耗成本,优化代码时也应该把此项作为的优化参考,比如:

func malloc()[]int{
    return make([]int, 1024)
}
func BenchmarkMalloc(b *testing.B){ 
    //b.ReportAllocs()       //无论是否使用-benchmem选项,都会输出内存分配信息                                                    
    for i:= 0; i < b.N;i++{
        _ = malloc()
    }   
}
输出结果包含单次测试分配的内存总量和次数
# go test -v file_test.go -bench . -benchmem -gcflags "-N -l"
BenchmarkMalloc-2   	 1000000	      2457 ns/op	    8192 B/op	       1 allocs/op
PASS
ok  	command-line-arguments	2.488s

代码覆盖率

除了执行单元测试和性能测试来测试代码质量之外,还应该度量测试用例自身的完整和有效性,也就是代码覆盖率。 通过代码覆盖率的值,可以分析出测试用例的编写质量,比如测试用例是否提供了足够多的测试条件,是否执行了 足够的函数、语句、分支和代码行等,以此来量化测试本身,使白盒测试真正起到项目代码质量保障作用。

# cat sum.go
package sum 
import "math"
func sum(x,y int)int{
    if x < 0{
        x = int(math.Abs(float64(x)))
    }   
    if y < 0{
        y = int(math.Abs(float64(y)))                                                          
    }   
    return x+y 
}
# cat sum_test.go
package sum
import "testing"
func TestSum(t *testing.T){
	if sum(1,2) !=3 {
		t.Fatal()
	}
}
输出:
# go test -cover
PASS
coverage: 60.0% of statements
ok  	_/root/data/workspace/src/test/sum	0.003s

可以指定covermode和coverprofile参数,获取更详细测试信息,比如:

- set: 是否执行
- count: 执行次数
- atomic: 类似于count,不过支持并发模式
# go test -cover -covermode=count -coverprofile cover.out

在浏览器中查看,将鼠标放在代码上可以查看该语句执行次数。

# go tool cover -html=cover.out

test

性能监控

是什么引发了性能问题呢?延迟过高、内存占用过多、意外阻塞等都是影响性能的表现, Go语言提供了两种性能监控方式帮助开发者定位问题原因:

- 测试时输出并保存相关数据,进行初期评估。
- 运行时通过WEB接口获取实时性能监控数据。

比如:

# go test -run NONE -bench . -memprofile mem.out -cpuprofile cpu.out net/tcp

在cpu.out 和 mem.out文件中分别保存了CPU和内存的采样数据。 参数描述如下:

注意在执行性能测试时,为了有足够长的采样时间,可以手动设定benchtime参数
  • pprof Go语言提供了pprof包来做代码的性能监控,可以使用pporf静态分析上面的性能测试结果,并且也可以 进行实时性能监控,pprof在Go标准库中存在于两个路径下:
/src/net/http/pprof
/src/runtime/pprof

其中net/http/pprof是对runtime/pprof包的封装,用户可以通过HTTP协议进行实时监控访问。 针对不同的程序类型,将采用不同的性能监控方式,比如:

  • Web服务

如果被监控程序是用http包启动的Web服务器,只需要引入net/http/pprof包即可,然后通过浏览器查看当前web服务器状态,比如:

package main
import (
	"net/http"
	_ "net/http/pprof"
)
func hello(w http.ResponseWriter, req *http.Request) {
	w.Write([]byte("Hello"))
}
func main() {
	http.HandleFunc("/hello", hello);
	http.ListenAndServe(":8001", nil);
}
输出:
通过 http://localhost:8001/debug/pprof/ 查看结果
  • 应用服务

如果被监控程序是个服务进程,使用net/http/pprof包,再额外开启一个Gorouting来运行 HTTP服务,比如:

package main
import (
	"time"
	"net/http"
	_ "net/http/pprof"
)
func main() {
	go http.ListenAndServe(":6060", nil);

	for{
		time.Sleep(time.Second * 3)
	}
}
输出:
通过 http://localhost:6060/debug/pprof/ 查看结果

上述测试也可以通过命令行获取采样结果:

# go tool pprof http://localhost:6060/debug/pprof/profile
# go tool pprof http://localhost:6060/debug/pprof/heap
# go tool pprof http://localhost:6060/debug/pprof/block
  • 应用程序

如果被测试程序是应用程序,那么就不能使用net/http/pprof包了,需要使用runtime/pprof, 具体用法是:

pprof.StartCPUProfile 和 pprof.StopCPUProfile 监控CPU的消耗
pprof.WriteHeapProfile 监控MEM的消耗
package main                                                                                   
import (
    "log"
    "flag"
    "os"
    "runtime/pprof"
    "sync"
    "time"
    "fmt"
)

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")

func sum(s []int, ch chan int, wg *sync.WaitGroup){
    defer wg.Done()
    var result int= 0
    for _, v := range s{
        result += v
    }   
    ch <- result
}

func readChan(ch chan int){
    for{
        select{
        case v,ok := <-ch:
            if ok{ 
                fmt.Println(ok,v)
            }   
        }   
    }   
}
func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }   
        defer f.Close()
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }   
    var wg sync.WaitGroup
    var ch = make(chan int)
    go readChan(ch)
    for i := 0;i < 100;i++{
        s := make([]int, 0)
        s = append(s,1,2,2,3,5)
        wg.Add(1)
        go sum(s,ch, &wg)
        time.Sleep(time.Second * 1)
    }   
    wg.Wait()
    close(ch)
    println("exit")
}
输出:
通过如下命令进入交互模式:
#go tool pprof [应用程序] [profile]
top命令可以看最耗时(内存)的function;各字段的含义如下:
    1. 采样点落在该函数中的次数
    2. 采样点落在该函数中的百分比
    3. 上一项的累积百分比
    4. 采样点落在该函数,以及被它调用的函数中的总次数
    5. 采样点落在该函数,以及被它调用的函数中的总次数百分比
    6. 函数名

web命令可以生成图片,更加直观分析性能