Go:测试
go test 工具
go test
是 Go 语言包的测试驱动程序 ,包依据特定约定组织 。包目录中以_test.go
结尾的文件是go test
编译对象,而非go build
的编译目标 。
特殊测试函数
在*_test.go
文件中有三种特殊函数 :
- 功能测试函数:以
Test
为前缀命名 ,用于检测程序逻辑正确性 ,go test
运行后报告测试结果为PASS
(通过)或FAIL
(失败 )。 - 基准测试函数:名称以
Benchmark
开头 ,用于测试某些操作性能 ,go test
会汇报操作的平均执行时间 。 - 示例函数:名称以
Example
开头 ,提供经机器检查的文档 。后续 11.2 节、11.4 节、11.6 节将分别详述这三种函数。
工作流程
go test
工具扫描*_test.go
文件查找上述特殊函数 ,生成临时main
包调用它们 ,接着进行编译、运行并汇报结果 ,最后清空临时文件 。
Test 函数
func TestName(t *testing.T) {//...
}
测试文件需导入testing
包 。功能测试函数以Test
为前缀,可选后缀以大写字母开头,函数签名为func TestName(t *testing.T)
,参数t
用于汇报测试失败和记录日志 。
package word
func IsPalindrome(s string) bool {for i := range s {if s[i]!= s[len(s)-1-i] {return false}}return true
}package word_test
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`)}
}
IsPalindrome
函数用于判断字符串是否为回文 。在word_test.go
文件中,有TestPalindrome
和TestNonPalindrome
两个测试函数,分别检查IsPalindrome
对回文和非回文输入的判断是否正确,通过t.Error
报告错误 。
测试运行与反馈
go test
(或go build
)在不指定包参数时,以当前目录所在包为参数 。运行测试命令后,可得到测试结果,如测试通过显示ok
及耗时;测试失败会给出具体错误信息,像新增TestFrenchPalindrome
和TestCanalPalindrome
测试函数后,因IsPalindrome
函数存在问题导致测试失败 。
测试选项
-v
选项:可输出包中每个测试用例的名称和执行时间,便于详细了解测试过程 。-run
选项:参数为正则表达式,能让go test
只运行函数名称匹配给定模式的测试函数 。
随机测试
基于表的测试针对精心挑选的输入检测函数,而随机测试通过构建随机输入扩展测试覆盖范围 。
随机测试策略
- 低效算法对比:额外编写一个使用低效但清晰算法的函数,对比两种实现的输出是否一致 。
- 模式构建输入:构建符合特定模式的输入,从而预知对应输出 。
注意事项
随机测试具有不确定性,测试用例失败时要记录足够信息以便重现问题 。对于复杂输入函数,记录伪随机数生成器种子比存储整个输入数据结构更简便 。使用当前时间作种子源,在自动化系统周期性运行测试时,每次运行都会得到新输入 。
测试命令
go test
工具不仅用于测试库代码包,也可测试命令 。包名main
一般生成可执行文件,但也能当作库导入进行测试 。
测试代码编写
package mainimport ("bytes""fmt""testing"
)func TestEcho(t *testing.T) {var tests = []struct {newline boolsep stringargs []stringwant string}{{true, "", []string{}, "\n"},{false, "", []string{}, ""},{true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"},{true, ",", []string{"a", "b", "c"}, "a,b,c\n"},{false, ":", []string{"1", "2", "3"}, "1:2:3"},}for _, test := range tests {descr := fmt.Sprintf("echo(%v, %q, %q)",test.newline, test.sep, test.args)out = new(bytes.Buffer) // 捕获的输出if err := echo(test.newline, test.sep, test.args); err!= nil {t.Errorf("%s failed: %v", descr, err)continue}got := out.(*bytes.Buffer).String()if got!= test.want {t.Errorf("%s = %q, want %q", descr, got, test.want)}}
}
在echo_test.go
中编写测试代码 :
- 导入相关包(
bytes
、fmt
、testing
),定义TestEcho
测试函数 。 - 使用结构体数组
tests
组织测试用例,每个用例包含newline
、sep
、args
、want
(预期输出 )字段 。 - 遍历测试用例,为每个用例创建
bytes.Buffer
捕获输出,调用echo
函数,若有错误用t.Errorf
报告;若无错,对比实际输出与预期输出,不一致时也用t.Errorf
报告 。
注意事项
- 测试代码和产品代码在同一包(
main
包 )中,测试时该包当作库,TestEcho
函数被测试驱动程序调用,main
函数被忽略 。 - 可通过添加测试用例扩展测试,文中展示添加错误预期结果的用例及测试失败输出 。
- 测试代码中避免调用
log.Fatal
和os.Exit
,以免阻止跟踪过程,预期错误应通过返回非空error
值报告 。
白盒测试
测试分类可依据对测试包内部了解程度。黑盒测试仅通过公开 API 和文档了解包,包内部逻辑不透明;白盒测试可访问包内部函数和数据结构,进行常规用户无法做到的观察和改动,如检查数据类型不可变性维护情况 。二者互补,黑盒测试健壮,程序更新后基本无需修改,能发现 API 设计缺陷;白盒测试可对实现特定之处作更详细覆盖测试 。
- 以
TestIsPalindrome
函数为例,仅调用导出的IsPalindrome
函数,属于黑盒测试 。 TestEcho
函数调用未导出的echo
函数并更新未导出的全局变量out
,属于白盒测试 。
外部测试包
以net/url
和net/http
包为例,net/http
依赖net/url
,若在net/url
包内声明测试函数,可能导致包循环引用(Go 规范禁止 ) 。为解决此问题,引入外部测试包 。在net/url
目录下,有url_test
包声明的文件,后缀_test
让go test
单独编译该包并运行测试 。外部测试包无法通过常规路径导入 。
外部测试在单独包中,可引用依赖被测包的帮助包,这是包内测试无法做到的 。从设计层次看,外部测试包在其依赖的包之上,能更自由地导入其他包进行测试,尤其适合集成测试 。
go list
工具:可汇总包目录中的文件类型 。- 特殊文件:有时外部测试包需特殊访问权限避免循环引用,会在包内测试文件(
_test.go
)添加函数声明,将包内部功能暴露给外部测试 ,仅为此目的且无自身测试的源文件一般叫export_test.go
。 。
编写有效测试
Go 测试框架极简,与其他语言测试框架不同 。其他语言框架借助反射或元数据识别测试函数,有测试 “启动” 和 “销毁” 钩子,提供断言、值比较、错误消息格式化等工具方法库 ,虽能让测试编写精细,但结果像用其他语言编写测试,且错误消息可能模糊、维护不友好 。
Go 期望测试编写者像编写普通程序一样,通过定义函数避免重复 。测试不是机械填表,测试过程要有良好用户界面(维护者即用户 ) 。好的测试在出错时不崩溃,能输出简洁清晰的现象描述及相关信息,助维护者定位问题;不应在首次失败时终止,而应尝试报告多个错误,从错误发生方式挖掘原因 。
避免脆弱的测试
如果程序在遇到新合法输入时常崩溃,说明程序有缺陷;若测试用例在程序可靠改动时奇怪失败,那么该测试用例是脆弱的 。最脆弱的测试在产品代码稍有改动(无论好坏 )就失败,这类测试被称为变化探测器或现状探测器 ,处理它们耗费的时间会抵消其带来的好处 。
当被测函数输出复杂(如长字符串、详细数据结构、文件等 )时,若在测试中检查输出完全匹配预期的 “幸运值” ,随着程序演进,输出内容或输入可能改变,导致测试失败 。比如输出部分内容可能向好改变,但因与测试预期不符而使测试不通过;复杂输入的函数也可能因测试输入不再合法而崩溃 。
避免脆弱测试的关键是仅检查关心的属性 。先测试程序中简单稳定的接口,再测内部函数 。设置断言时要有选择性,例如不追求字符串精确匹配,而是寻找程序进化中不变的子串 。还可编写稳定函数从复杂输出提取核心内容,使断言更可靠 。虽然前期需投入精力,但可减少修复奇怪失败测试的时间消耗。
覆盖率
测试旨在发现 bug 而非证明其不存在,再多测试也无法证明包无 bug ,但测试可增强对包在多种场景下可用的信心 。测试套件覆盖待测包的比例即测试覆盖率 ,它无法精确测量,因程序动态性,微小程序也难精准衡量,但可辅助将测试精力集中在关键处 。语句覆盖率是常用的简单方法,指部分语句在一次执行中至少执行一次 。
go test
相关命令:检测测试可通过$ go test -v -run=Coverage gopl.io/ch7/eval
,加上-coverprofile=c.out
标记运行测试,可启用覆盖数据收集,修改源代码副本,在语句块执行前设置布尔变量,执行结束将变量值写入c.out
日志文件并输出执行语句汇总信息 。go tool cover
工具:go tool cover
可处理生成的日志,生成 HTML 报告 。如$ go tool cover -html=c.out
,在报告界面中,绿色(浅灰色 )标记的语句块表示被覆盖,红色(加阴影深灰色 )表示未被覆盖 。一元操作符Eval
方法未被覆盖,添加新测试用例可使其被覆盖;panic
语句未覆盖属正常,因不应执行到 。
Benchmark 函数
基准测试是在一定工作负载下检测程序性能的方法 。在 Go 中,基准测试函数名以Benchmark
为前缀,拥有*testing.B
参数,提供与性能检测相关方法,还包含整数型变量N
,用于指定被检测操作的执行次数 。
基准测试运行器开始时因不确定操作耗时,先用较小N
值检测,再推断出足够大的N
值以检测稳定运行时间 。在基准测试函数中实现循环,可在循环外执行必要初始化代码且不影响每次迭代时间 ,testing.B
参数提供停止、恢复和重置计时器的方法 。
基准测试不仅告知给定操作绝对耗时,还用于比较不同操作相对耗时 。如处理不同数量元素耗时比较、I/O 缓冲区最佳大小选择、不同算法性能对比等 。性能比较函数可被多个Benchmark
函数传入不同值调用 ,编写时注意不要用b.N
作为输入大小,除非作为固定大小输入的循环次数 。基准测试在程序设计及演进过程中都有重要作用 。
性能剖析
基准测试对检测操作性能有帮助,但优化程序时,不能盲目过早优化 。多数情况下,过度关注微小性能提升是浪费时间,约 97% 场景中过早优化有害 。但也不能错过关键的 3%,需仔细排查关键代码,而不能仅凭直觉认定,性能剖析是发现关键代码的最佳技术 。
性能剖析通过自动化手段,基于程序执行中性能事件采样进行性能评测,生成性能剖析报告 。Go 支持多种性能剖析方式,每种与不同性能指标相关,且需记录相关事件及对应栈信息 :
- CPU 性能剖析:识别执行过程中占用 CPU 多的函数 。CPU 上执行线程定时被操作系统中断,中断时记录性能剖析事件 。
- 堆性能剖析:找出分配内存多的语句 。性能剖析库对内存分配调用采样,每个事件平均记录分配 512KB 内存 。
- 阻塞性能剖析:识别阻塞协程久的操作,如系统调用、通道数据收发、获取锁等 。协程被阻塞时记录事件 。
获取与分析性能剖析报告
- 获取报告:通过
go test
指定标记获取性能剖析报告,如-cpuprofile=cpu.out
、-blockprofile=block.out
、-memprofile=mem.out
。同时使用多个标记时,获取一种报告机制会覆盖其他类别报告 。非测试程序也可通过runtime API
启用性能剖析 。 - 分析报告:用
pprof
工具分析报告,通过go tool pprof
间接使用 。基本用法需传入可执行文件和性能剖析日志 。因性能剖析日志不含函数名,需结合可执行文件理解数据 。go test
启用性能剖析时会保存可执行文件并命名为foo.test
(foo
为被测包名 ) 。
Example 函数
func ExampleIsPalindrome() {fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))fmt.Println(IsPalindrome("palindrome"))// 输出 :// true// false
}
示例函数是go test
特殊对待的函数,以Example
开头 ,无参数无结果 。文中给出ExampleIsPalindrome
示例函数,用于展示IsPalindrome
函数功能,通过打印该函数对不同字符串的判断结果,并注释预期输出 。
示例函数目的
- 作为文档:是描述库函数功能简洁直观的方式,可演示 API 中类型和函数交互 。基于
Example
函数后缀,godoc
文档服务器可将其与所演示函数或包关联 ,如ExampleIsPalindrome
和IsPalindrome
函数文档关联 。 - 可执行测试:若示例函数最后含
// 输出 :
注释,go test
运行时会执行该函数并检查输出与注释文本是否匹配 。 - 提供手动实验代码:
godoc
文档服务器利用 Go Playground 让用户在 Web 浏览器编辑运行示例函数,方便了解函数功能或语言特性 。
参考资料:《Go程序设计语言》