Golang 学习笔记十五 测试

参考《Go语言圣经》P395

一、概述

go test命令是一个按照一定的约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源文件并不是go build构建包的一部分,它们是go test测试的一部分。在*_test.go文件中,有三种类型的函数:测试函数、基准测试函数、示例函数。

  • 1.一个测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。

  • 2.基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准函数以计算一个平均的执行时间。

  • 3.示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

二、测试函数

每个测试函数必须导入testing包。测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头。

//word1.go
package word

// IsPalindrome reports whether s reads the same forward and backward.
// (Our first attempt.)
func IsPalindrome(s string) bool {
    for i := range s {
        if s[i] != s[len(s)-1-i] {
            return false
        }
    }
    return true
}


//word_test.go
package word

import "testing"
func TestPalindrome(t *testing.T) {
    if !IsPalindrome("detartrated") {
        t.Error(`IsPalindrome("detartrated") = false`)
    }
    if !IsPalindrome("kayak") {
        t.Error(`IsPalindrome("kayak") = false`)
    }
}
func TestNonPalindrome(t *testing.T) {
    if IsPalindrome("palindrome") {
        t.Error(`IsPalindrome("palindrome") = true`)
    }
}

func TestFrenchPalindrome(t *testing.T) {
    if !IsPalindrome("été") {
        t.Error(`IsPalindrome("été") = false`)
    }
}

func TestCanalPalindrome(t *testing.T) {
    input := "A man, a plan, a canal: Panama"
    if !IsPalindrome(input) {
        t.Errorf(`IsPalindrome(%q) = false`, input)
    }
}

测试命令如下

$ go test
--- FAIL: TestFrenchPalindrome (0.00s)
word_test.go:28: IsPalindrome("été") = false
--- FAIL: TestCanalPalindrome (0.00s)
word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false
FAIL
FAIL gopl.io/ch11/word1 0.014s

参数 -v 可用于打印每个测试函数的名字和运行时间。

参数 -run 对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被 go test 测试命令运行:$ go test -v -run="French|Canal"

我们现在的任务就是修复这些错误。简要分析后发现第一个BUG的原因是我们采用了 byte而
不是rune序列,所以像“été”中的é等非ASCII字符不能正确处理。第二个BUG是因为没有忽略
空格和字母的大小写导致的。

关于rune和byte区别,可以参考Golang 学习笔记三 字符串 字典,这里使用rune切片重新处理,并使用unicode.IsLetter过滤掉非字母,使用unicode.ToLower过滤大小写。

func IsPalindrome(s string) bool {
    var letters []rune
    for _, r := range s {
        if unicode.IsLetter(r) {
            letters = append(letters, unicode.ToLower(r))
        }
    }
    for i := range letters {
        if letters[i] != letters[len(letters)-1-i] {
            return false
        }
    }
    return true
}

然后合并测试数据:

func TestIsPalindrome(t *testing.T) {
    var tests = []struct {
        input string
        want bool
    }{
        {"", true},
        {"a", true},
        {"aa", true},
        {"ab", false},
        {"kayak", true},
        {"detartrated", true},
        {"A man, a plan, a canal: Panama", true},
        {"Evil I did dwell; lewd did I live.", true},
        {"Able was I ere I saw Elba", true},
        {"été", true},
        {"Et se resservir, ivresse reste.", true},
        {"palindrome", false}, // non-palindrome
        {"desserts", false}, // semi-palindrome
    }
    
    for _, test := range tests {
        if got := IsPalindrome(test.input); got != test.want {
            t.Errorf("IsPalindrome(%q) = %v", test.input, got)
        }
    }
}

这种表格驱动的测试在Go语言中很常见的。我们很容易向表格添加新的测试数据,并且后面的测试逻辑也没有冗余,这样我们可以有更多的精力地完善错误信息。

失败测试的输出并不包括调用t.Errorf时刻的堆栈调用信息。和其他编程语言或测试框架的assert断言不同,t.Errorf调用也没有引起panic异常或停止测试的执行。即使表格中前面的数据导致了测试的失败,表格后面的测试数据依然会运行测试,因此在一个测试中我们可能了解多个失败的信息。

如果我们真的需要停止测试,或许是因为初始化失败或可能是早先的错误导致了后续错误等原因,我们可以使用t.Fatal或t.Fatalf停止当前测试函数。它们必须在和测试函数同一个goroutine内调用。

二、基准测试

基准测试是测量一个程序在固定工作负载下的性能。在Go语言中,基准测试函数和普通测试函数写法类似,但是以Benchmark为前缀名,并且带有一个 *testing.B 类型的参数; *testing.B 参数除了提供和 *testing.T 类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。

和单元测试文件一样,基准测试的文件名也必须以_test.go 结尾。同时也必须导入testing 包。

下面是IsPalindrome函数的基准测试,其中循环将执行N次。

import "testing"

func BenchmarkIsPalindrome(b *testing.B) {
    for i := 0; i < b.N; i++ {
        IsPalindrome("A man, a plan, a canal: Panama")
    }
}

我们用下面的命令运行基准测试。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过 -bench 命令行标志参数手工指定要运行的基准测试函数。该参数是一个正则表达式,用于匹配要执行的基准测试函数的名字,默认值是空的。其中“.”模式将可以匹配所有基准测试函数,但是这里总共只有一个基准测试函数,因此和 -bench=IsPalindrome 参数是等价的效果。

$ cd $GOPATH/src/gopl.io/ch11/word2
$ go test -bench=.
PASS
BenchmarkIsPalindrome-8 1000000 1035 ns/op
ok gopl.io/ch11/word2 2.179s

结果中基准测试名的数字后缀部分,这里是8,表示运行时对应的GOMAXPROCS的值,这对于一些和并发相关的基准测试是重要的信息。

报告显示每次调用IsPalindrome函数花费1.035微秒,是执行1,000,000次的平均时间。因为基准测试驱动器开始时并不知道每个基准测试函数运行所花的时间,它会尝试在真正运行基准测试前先尝试用较小的N运行测试来估算基准测试函数所需要的时间,然后推断一个较大的时间保证稳定的测量结果。

循环在基准测试函数内实现,而不是放在基准测试框架内实现,这样可以让每个基准测试函数有机会在循环启动前执行初始化代码,这样并不会显著影响每次迭代的平均运行时间。如果还是担心初始化代码部分对测量时间带来干扰,那么可以通过testing.B参数提供的方法来临时关闭或重置计时器,不过这些一般很少会用到。(b.ResetTimer())

现在我们有了一个基准测试和普通测试,我们可以很容易测试新的让程序运行更快的想法。也许最明显的优化是在IsPalindrome函数中第二个循环的停止检查,这样可以避免每个比较都做两次。不过很多情况下,一个明显的优化并不一定就能代码预期的效果。这个改进在基准测试中只带来了4%的性能提升。

另一个改进想法是在开始为每个字符预先分配一个足够大的数组,这样就可以避免在append调用时可能会导致内存的多次重新分配。声明一个letters数组变量,并指定合适的大小,像下面这样

letters := make([]rune, 0, len(s))
    for _, r := range s {
        if unicode.IsLetter(r) {
        letters = append(letters, unicode.ToLower(r))
    }
}

这个改进提升性能约35%,报告结果是基于2,000,000次迭代的平均运行时间统计。如这个例子所示,快的程序往往是伴随着较少的内存分配。 -benchmem 命令行标志参数将在报告中包含内存的分配数据统计。我们可以比较优化前后内存的分配情况:

$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome 1000000 1026 ns/op 304 B/op 4 allocs/op

这是优化之后的结果:

$ go test -bench=. -benchmem
PASS
BenchmarkIsPalindrome 2000000 807 ns/op 128 B/op 1 allocs/op

用一次内存分配代替多次的内存分配节省了75%的分配调用次数和减少近一半的内存需求。

三、示例函数

第三种 go test 特别处理的函数是示例函数,以Example为函数名开头。示例函数没有函数参数和返回值。下面是IsPalindrome函数对应的示例函数:

func ExampleIsPalindrome() {
    fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
    fmt.Println(IsPalindrome("palindrome"))
    // Output:
    // true
    // false
}

示例函数有三个用处。最主要的一个是作为文档:一个包的例子可以更简洁直观的方式来演示函数的用法,比文字描述更直接易懂,特别是作为一个提醒或快速参考时。一个示例函数方便展示属于同一个接口的几种类型或函数直接的关系,所有的文档都必须关联到一个地方,就像一个类型或函数声明都统一到包一样。同时,示例函数和注释并不一样,示例函数是完整真实的Go代码,需要接受编译器的编译时检查,这样可以保证示例代码不会腐烂成不能使用的旧代码。

根据示例函数的后缀名部分,godoc的web文档会将一个示例函数关联到某个具体函数或包本身,因此ExampleIsPalindrome示例函数将是IsPalindrome函数文档的一部分,Example示例函数将是包文档的一部分。

示例文档的第二个用处是在 go test 执行测试的时候也运行示例函数测试。如果示例函数内含有类似上面例子中的 // Output: 格式的注释,那么测试工具会执行这个示例函数,然后检测这个示例函数的标准输出和注释是否匹配。

示例函数的第三个目的提供一个真实的演练场。 http://golang.org 就是由godoc提供的文档服务,它使用了Go Playground提高的技术让用户可以在浏览器中在线编辑和运行每个示例函数,就像图11.4所示的那样。这通常是学习函数使用或Go语言特性最快捷的方式。

四、模仿调用--《Go语言实战》第9章

不能总是假设运行测试的机器可以访问互联网。此外,依赖不属于你的或者你无法操作的服务来进行测试,也不是一个好习惯。这两点会严重影响测试持续集成和部署的自动化。如果突然断网,导致测试失败,就没办法部署新构建的程序。

为了修正这个问题,标准库包含一个名为 httptest 的包,它让开发人员可以模仿基于HTTP 的网络调用。模仿(mocking)是一个很常用的技术手段,用来在运行测试时模拟访问不可用的资源。包 httptest 可以让你能够模仿互联网资源的请求和响应。在我们的单元测试中,通过模仿 http.Get 的响应,我们可以解决在图 9-4 中遇到的问题,保证在没有网络的时候,我们的测试也不会失败,依旧可以验证我们的 http.Get 调用正常工作,并且可以处理预期的响应。让我们看一下基础单元测试,并将其改为模仿调用 goinggo.net 网站的 RSS 列表,如代码清单 9-12 所示。

代码清单 9-12 listing12_test.go:第 01 行到第 41 行
01 // 这个示例程序展示如何内部模仿 HTTP GET 调用
02 // 与本书之前的例子有些差别
03 package listing12
04
05 import (
06  "encoding/xml"
07  "fmt"
08  "net/http"
09  "net/http/httptest"
10  "testing"
11 )
12
13 const checkMark = "\u2713"
14 const ballotX = "\u2717"
15
16 // feed 模仿了我们期望接收的 XML 文档
17 var feed = `<?xml version="1.0" encoding="UTF-8"?>
18 <rss>
19 <channel>
20  <title>Going Go Programming</title>
21  <description>Golang : https://github.com/goinggo</description>
22  <link>http://www.goinggo.net/</link>
23  <item>
24  <pubDate>Sun, 15 Mar 2015 15:04:00 +0000</pubDate>
25  <title>Object Oriented Programming Mechanics</title>
26  <description>Go is an object oriented language.</description>
27  <link>http://www.goinggo.net/2015/03/object-oriented</link>
28  </item>
29 </channel>
30 </rss>`
31
32 // mockServer 返回用来处理请求的服务器的指针
33 func mockServer() *httptest.Server {
34  f := func(w http.ResponseWriter, r *http.Request) {
35  w.WriteHeader(200)
36  w.Header().Set("Content-Type", "application/xml")
37  fmt.Fprintln(w, feed)
38  }
39
40  return httptest.NewServer(http.HandlerFunc(f))
41 }
五、备注

1.我在main文件夹下放了一个叉叉test.go,但是运行报错啊:# main.test C:\Users\lenovo\AppData\Local\Temp\go-build612135217\b056\_testmain.go:12:2: cannot import "main"
百度到这个Go学习笔记

注: 不要将代码放在名为 main 的目录下,这会导致 go test "cannot import main" 错误。

移出main文件夹,果然好用了。

六、测试框架stretchr/testify

参考搞定Go单元测试(三)—— 断言(testify)

我们先来看看Go标准包中为什么没有断言,官方在FAQ里面回答了这个问题。

golang.org/doc/faq#ass…

总体概括一下大意就是:“Go不提供断言,我们知道这会带来一定的不便,其主要目的是为了防止你们这些程序员在错误处理上偷懒。我们知道这是一个争论点,但是我们觉得这样很coooool~~。

所以,我们引入断言库的原因也很明显了:偷懒,引入断言能为我们提供便利——提高测试效率,增强代码可读性。

在断言库的选择上,我们似乎没有过多的选择,从start数和活跃度来看,基本上是testify一枝独秀。

github.com/stretchr/te…

没有对比就没有伤害,先来看看使用testify之前的测试方法:

func TestSomeFun(t *testing.T){
...
    if v != want {
        t.Fatalf("v值错误,期望值:%s,实际值:%s", want, v)
    }
    if err != nil {
        t.Fatalf("非预期的错误:%s", err)
    }
    if objectA != objectB {
        if objectA.field1 !=  objectB.field1 {
            // t.Fatalf() field1值错误...bla bla bla
        }
         if objectA.field2 !=  objectB.field2 {
            // t.Fatalf() field2值错误...bla bla bla
        }
        // 遍历object所有值... bla bla bla
    }
...
}
复制代码

上述代码充斥着大量if...else..判断,大段错误信息拼装(真·体力活...),运气不好碰到结构体判断要得将其遍历一遍——不直观,低效,实在是不fashion。
现在,我们使用testify来改造一下上面的测试示例:

func TestSomeFun(t *testing.T){
    a := assert.New(t)
...
    a.Equal(v, want)
    a.Nil(err,"如果你还是想输出自己拼装的错误信息,可以传第三个参数")
    a.Equal(objectA, objectB)
...
}
复制代码

三行搞定,测试含义一目了然——直观,高效,简短,fashion。

参考golang的测试框架stretchr/testify

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

assert和require的唯一差别就是require的函数会直接导致case结束,而assert虽然也标记为case失败,但case不会退出,而是继续往下执行。

更多可以参考
go 用testify搭建完整易用的测试环境
用 Testify 来改善 GO 测试和模拟
使用testify和mockery库简化单元测试

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

推荐阅读更多精彩内容