《Go语言四十二章经》第二十六章 测试

《Go语言四十二章经》第二十六章 测试

作者:李骁

在Go语言中,所有的包都应该有必要文档和注释,当然同样甚至更为重要的是对包进行必要的测试。

testing 包就是这样一个标准包,被专门用来进行单元测试以及进行自动化测试,打印日志和错误报告,方便程序员调试代码,并且还包含一些基准测试函数的功能。

testing 包含测试函数、测试辅助代码和示例函数;测试函数包括Test开头的单元测试函数和以Benchmark开头的基准测试函数两种,测试辅助代码是为测试函数服务的公共函数、初始化函数、测试数据等,而示例函数则是以Example开头的说明被测试函数用法的函数,而示例函数通常被保存在example_*_test.go文件中。

26.1 单元测试

开发中经常需要对一个包做(单元)测试,写一些可以频繁(每次更新后)执行的小块测试单元来检查代码的正确性,于是我们必须写一些 Go 源文件来测试代码。

使用testing包,我们只需要遵守简单的规则,就可以很好地写出通用的测试程序。因为其他开发人员也会遵循这个包的规则来进行测试。

首先测试程序是独立的文件,他必须属于被测试的包,和这个包的其他程序放在一起,并且文件名满足这种形式 *_test.go。由于是独立的测试文件,所以测试代码和包中的业务代码是分开的。Go语言这样规定的好处是不言而喻的,因为在其他语言开发的程序中,我们经常可以看到代码中注释掉的测试代码,而且有把开发版作为生产版发布到线上导致异常的问题出现。

当然,好的规则需要我们遵守并严格执行。

_test 程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 Gotest 会编译所有的程序:普通程序和测试程序。

测试文件中必须导入 "testing" 包,测试函数名字是以 TestXxx 打头的全局函数,Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],函数名我们可以以被测试函数的字母描述,如 TestFmtInterface,TestPayEmployees 等。测试用例会按照测试源代码中写的顺序依次执行。

测试函数一般都要求这种形式的头部:

func TestAbcde(t *testing.T)

*testing.T是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。t.Log函数就像我们常常使用的fmt.Println一样,可以接受多个参数,方便输出调试结果。

用下面这些函数来通知测试失败:
1)func (t *T) Fail()
标记测试函数为失败,然后继续执行剩下的测试。

2)func (t *T) FailNow()
标记测试函数为失败并中止执行;文件中别的测试也被略过,继续执行下一个文件。

3)func (t *T) Log(args ...interface{})
args 被用默认的格式格式化并打印到错误日志中。

4)func (t *T) Fatal(args ...interface{})
结合 先执行 3),然后执行 2)的效果。

运行 Go test 来编译测试程序,并执行程序中所有的 TestXxx 函数。如果所有的测试都通过会打印出 PASS。

当然,对于包中不能导出的函数不能进行单元或者基准测试。

Gotest 可以接收一个或多个函数程序作为参数,并指定一些选项。

在系统标准包中,有很多 _test.go 结尾的程序,大家可以用来测试,为节约篇幅这里我就不写具体例子了。

26.2 基准测试

testing 包中有一些类型和函数可以用来做简单的基准测试;测试代码中必须包含以 BenchmarkZzz 打头的函数并接收一个 *testing.B 类型的参数,比如:

func BenchmarkReverse(b *testing.B) {
    ...
}

命令 Go test –test.bench=.* 会运行所有的基准测试函数;代码中的函数会被调用 N 次(N是非常大的数,如 N = 1000000),可以根据情况指定b.Z的值,并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark 调用这些函数,直接运行程序即可。

下面我们看一个测试的具体例子:

package even

func Loop(n uint64) (result uint64) {
    result = 1
    var i uint64 = 1
    for ; i <= n; i++ {
        result *= i
    }
    return result
}

func Factorial(n uint64) (result uint64) {
    if n > 0 {
        result = n * Factorial(n-1)
        return result
    }
    return 1
}

在 even 包的路径下,我们创建一个名为 even_test.go 的测试程序:

package even

import (
    "testing"
)

func TestLoop(t *testing.T) {
    t.Log("Loop:", Loop(uint64(32)))
}

func TestFactorial(t *testing.T) {
    t.Log("Factorial:", Factorial(uint64(32)))
}

func BenchmarkLoop(b *testing.B) {

    for i := 0; i < b.N; i++ {
        Loop(uint64(40))
    }
}

func BenchmarkFactorial(b *testing.B) {

    for i := 0; i < b.N; i++ {
        Factorial(uint64(40))
    }
}

现在我们可以在这个包的目录下使用命令:Go test -test.bench=.* 来测试 even 包。

输出:

输出:

goos: windows
goarch: amd64
pkg: go42/chapter-13/13.1/1
BenchmarkLoop-4         50000000            27.2 ns/op
BenchmarkFactorial-4    10000000           163 ns/op
PASS
ok      go42/chapter-13/13.1/1  3.628s

递归函数的确是很耗费系统资源,而且运行也慢,不建议使用。

26.3 分析并优化 Go 程序

如果代码使用了 Go 中 testing 包的基准测试功能,我们可以用 Gotest 标准的 -cpuprofile 和 -memprofile 标志向指定文件写入 CPU 或 内存使用情况报告。

使用方式:

go test -x -v -test.cpuprofile=pprof.out

运行上面代码,将会基于基准测试把执行结果中的 cpu 性能分析信息写到 pprof.out 文件中。我们可以根据这个文件做分析来详细了解性能情况。

26.4 用 pprof 调试

要监控Go程序的堆栈,cpu的耗时等性能信息,我们可以通过使用pprof包来实现。在代码中,pprof包有两种方式导入:

"net/http/pprof"
"runtime/prof"

其实net/http/pprof中只是使用runtime/pprof包来进行封装了一下,并在http端口上暴露出来,让我们可以在浏览器查看程序的性能分析。我们可以自行查看net/http/pprof中代码,只有一个文件pprof.go。

下面我们具体说说怎么使用pprof,首先我们讲讲在开发中取得pprof信息的三种方式:

一:web 服务器程序

如果我们的Go程序是web服务器,你想查看自己的web服务器的状态。这个时候就可以选择net/http/pprof。你只需要引入包_"net/http/pprof",然后就可以在浏览器中使用http://localhost:port/debug/pprof/直接看到当前web服务的状态,包括CPU占用情况和内存使用情况等。

这里port是8080,也就是我们web服务器监听的端口。

package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"  // 为什么用_ , 在讲解http包时有解释。
)

func myfunc(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hi")
}

func main() {
    http.HandleFunc("/", myfunc)
    http.ListenAndServe(":8080", nil)
}

访问http://localhost:8080/debug/pprof/

二:服务进程

如果你的Go程序不是web服务器,而是一个服务进程,可以选择使用net/http/pprof包,然后开启一个goroutine来监听相应端口。

package main

import (
    "fmt"
    "log"
    "net/http"
    _ "net/http/pprof"

    "time"
)

func main() {
    // 开启pprof
    go func() {
        log.Println(http.ListenAndServe("localhost:8080", nil))
    }()
    go hello()
    select {}
}
func hello() {
    for {
        go func() {
            fmt.Println("hello word")
        }()
        time.Sleep(time.Millisecond * 1)
    }
}

访问http://localhost:8080/debug/pprof/
在前面这两种方式中,我们还可以在命令行分别运行以下命令:

利用这个命令查看堆栈信息:
go tool pprof http://localhost:8080/debug/pprof/heap
利用这个命令可以查看程序CPU使用情况信息:
go tool pprof http://localhost:8080/debug/pprof/profile
使用这个命令可以查看block信息:
go tool pprof http://localhost:8080/debug/pprof/block

[图片上传失败...(image-ecf876-1550975959275)]

这里需要先安装graphviz,http://www.graphviz.org/download/ ,windows平台直接下载zip包,解压缩后把bin目录放到$path中。我们可以通过执行命令 png 产生图片,还有svg,gif,pdf等命令,生成的图片自动命名存放在当前目录下,我们这里生成了png。其他命令使用可通过help查看。

三:应用程序

如果你的Go程序只是一个应用程序,那么你就不能使用net/http/pprof包了,你就需要使用到runtime/pprof。比如下面的例子:

package main

import (
    "flag"
    "fmt"
    "log"

    "os"
    "runtime/pprof"
    "time"
)

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

func Factorial(n uint64) (result uint64) {
    if n > 0 {
        result = n * Factorial(n-1)
        return result
    }
    return 1
}

func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal(err)
        }
        pprof.StartCPUProfile(f)
        defer pprof.StopCPUProfile()
    }

    go compute()
    time.Sleep(10 * time.Second)
}
func compute() {
    for i := 0; i < 100; i++ {
        go func() {
            fmt.Println(Factorial(uint64(40)))
        }()
        time.Sleep(time.Millisecond * 1)
    }
}

编译后生成3.exe文件并运行:

3.exe --cpuprofile=cpu.prof

这里我们编译后可执行程序是3.exe , 程序运行完后的cpu信息就会记录到cpu.prof中。

现在有了cpu.prof 文件,我们就可以通过go tool pprof 来看相应的信息了。在命令行运行:

go tool pprof 3.exe cpu.prof 

这里要注意的是需要带上可执行的程序名以及prof信息文件。

命令执行后会进入到:

132.png

命令界面和前面两种使用net/http/pprof包 一样。我们可以通过go tool pprof 生svg,png或者是pdf文件。

这是生成的png文件,和前面生成的png类似,前面我们生成的是block信息:

profile001.png

通过上面这三种情况的分析,我们可以知道,其实就是两种情况:
go tool pprof http://localhost:8080/debug/pprof/profile 这种url方式,或者
go tool pprof 3.exe cpu.prof 这种文件方式来进行分析。

我们可以根据项目情况灵活使用。有关pprof,我们就讲这么多,在实际项目中,我们多使用就会发现这个工具还是蛮有用处的。

本书《Go语言四十二章经》内容在github上同步地址:https://github.com/ffhelicopter/Go42
本书《Go语言四十二章经》内容在简书同步地址: https://www.jianshu.com/nb/29056963

虽然本书中例子都经过实际运行,但难免出现错误和不足之处,烦请您指出;如有建议也欢迎交流。
联系邮箱:roteman@163.com

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容