前言
我们都知道,越早发现问题越早纠正,那么程序的成本将更小。
每个语言都有针对自身代码的单元测试框架,这里简单做些对比介绍。
go简化了并发程序调试,goroutines是轻量级线程,动态调整栈大小。
测试函数以Test
开头,
基准测试用Benchmark
,
模糊测试用Fuzz
。TestMain
用于自定义测试设置。ReportMetric
添加自定义基准性能度量。RunParallel
用于并行基准测试。
1 go 的内建框架: 调试并发程序
go是 多线程的, 也就是基于消息传递的并发。
多线程和基于消息的并发编程是等价的。 多线程并发模型可以容易对应到多核处理器,主流操作系统也提供了系统级的多线程支持。
go 语言是基于消息并发模型的集大成者,它将基于CSP 模型的并发编程内置到语言中,通过一个go关键字 轻易启动 一个例程。 并且共享内存。
可以说 goroutine是 用户态的 ,与 系统线程不是等价的,也可以说它是一个轻量级的线程。
每个系统线程都有一个固定大小的栈,一般默认2MB,这个栈大小导致了两个问题:一个对于很多只需要很小的栈空间的线性是巨大浪费,二是对于少数需要巨大栈空间的线程又面临栈溢出的风险。
因此要么降低固定栈大小,提升空间利用率,要么增大栈大小允许更深的函数递归调用,但这无法兼有。
goroutine 以一个很小的栈启动 可能是 2KB 4KB,遇到深度递归导致栈空间不足时。
goroutine 根据需要动态伸缩栈大小。 最大可达 1GB
启动代价很小,就如调用函数一样简单,而例程之间的调度代价也很低,而伸缩空间很大,这极大促进了并发编程的流行和发展。
其调度使用半抢占式调度。调度器根据具体函数只保留必要寄存器,切换的代价比系统线程低得多。 运行时有一个 runtime.GOMAXPROCS 变量,用于控制当前允许正常非阻塞 例程的系统线程数目。
并发程序基准测试在其他语言中可能是个麻烦事情,在golang中,内建单测包提供了丰富的支持。
2 内置 测试框架 testing
基于xUnit套件层次结构,顺序结构 用于组织测试代码之间的结构。
那么在测试用例代码内部其一般逻辑是怎样的?
go测函数就是一个普通的go函数,仅对测试函数的函数名和函数原型有特定要求, 在测试函数TestXXX会其子测试函数 subtest, 如何执行测试逻辑并没有显式约束。
对失败与否的判断在于测试代码逻辑是否进入了包含 Error/Errorf, Fatal/Fatalf 等方法调用
一旦进入这些分支,即代表该测试失败。
不同的是 error/Errorf 并不会立刻终止当前 goroutine 的执行,还会继续执行 例程的后续测试。
而Fatal/Fatalf将立刻结束当前例程的执行。
这里介绍testing功能和代码结构。
内置的性能基准数据,它有内置的pprof 和 expvar 包支持导出导入go应用的数据,有接口或json的方式可用。
基准性能测试 形式如下
func BenchmarkXxx(* testing.B)
被视为基准测试,并在提供其 -bench 标志时由“go test”命令执行。基准测试按顺序运行。
示例基准函数如下所示:
func BenchmarkRandInt(b *testing.B) {
for i := 0;i < bN; i++ {
rand.Int()
}
}
3 基准结果 ReportMetric testing.B
ReportMetric 将“n unit”添加到报告的基准测试结果中。
如果度量是每次迭代,调用者应该除以 bN,并且按照惯例,输出的报告以“/op”结尾。
ReportMetric 覆盖任何先前报告的相同单位的值。如果 unit 是空字符串或 unit 包含任何空格,则 ReportMetric 会发生混乱。
如果单位是基准框架本身通常报告的单位(例如“allocs/op”),ReportMetric 将覆盖该指标。
将“ns/op”设置为 0 将抑制该内置指标。
如下这个 排序的例子提供了一个报告与特定算法相关的自定义基准性能度量,它之间调用包内的函数:
b.ReportMetric 它除以 b.N 得到平均值,并输出到执行报告的 /op 单元.
基准测试例子
package main import ( "sort" "testing" ) func main() { testing.Benchmark(func(b * testing.B) { var compares int64 for i := 0; i < b.N; i++ { s := []int{5, 4, 3, 2, 1} sort.Slice(s, func(i, j int) bool { compares++ return s[i] < s[j] }) } b.ReportMetric(float64(compares)/float64(b.N), "compares/op") }) }
基准函数必须运行目标代码 bN 次。在基准执行期间,调整 bN 直到基准函数持续足够长的时间以可靠地计时
并行基准测试 RunParallel testing.PB
RunParallel 并行运行基准测试。它创建多个 goroutines 并在它们之间分配 bN 次迭代。
goroutines 的数量默认为 GOMAXPROCS。要增加非 CPU 绑定基准测试的并行性,请在 RunParallel 之前调用 SetParallelism 。
RunParallel 通常与 go test -cpu 标志一起使用。
如下例子在单个对象提供 text/template.Template.Execute
RunParallel 将长久 GOMAXPROCS 例程并分散任务到它们。 每个例程有自己的字节缓冲。for pb.Next() 循环体在全部例程中共被执行 b.N次
package main import ( "bytes" "testing" "text/template" ) func main() { testing.Benchmark(func(b * testing.B) { templ := template.Must(template.New("test").Parse("Hello, {{.}}!")) b.RunParallel(func(pb * testing.PB) { var buf bytes.Buffer for pb.Next() { buf.Reset() templ.Execute(&buf, "World") } }) }) }
主体函数将在每个 goroutine 中运行。它应该设置任何 goroutine-local 状态,然后迭代直到 pb.Next 返回 false。它不应使用 StartTimer、StopTimer 或 ResetTimer 函数,因为它们具有全局作用。它也不应该调用 Run。
4 模糊测试 FuzzXxx testing.F 和 主要测试 testing.M
- 模拟测试
“go test”和测试包支持模糊测试,这是一种测试技术,其中使用随机生成的输入调用函数以查找单元测试未预料到的错误。
功能函数定义形式:
func FuzzXxx(* testing.F)
被认为是模糊测试。
主要测试 TestMain testing.M
测试或基准测试程序有时需要在执行之前或之后进行额外的设置或拆卸。
有时还需要控制哪些代码在主线程上运行。为了支持这些和其他情况,如果测试文件包含一个函数:func TestMain(m * testing.M)
那么生成的测试将调用 TestMain(m) 而不是直接运行测试或基准测试。TestMain 在主 goroutine 中运行,并且可以围绕对 m.Run 的调用进行任何必要的设置和拆卸。
m.Run 将返回一个可以传递给 os.Exit 的退出代码。如果 TestMain 返回,测试包装器会将 m.Run 的结果传递给 os.Exit 本身。
调用 TestMain 时,flag.Parse 尚未运行。如果 TestMain 依赖于命令行标志,包括测试包的标志,它应该显式调用 flag.Parse。命令行标志始终由运行的时间测试或基准函数解析。
TestMain 的一个简单实现是:
func TestMain(m * testing.M) { // 如果 TestMain 使用标志,则在此处调用 flag.Parse() os.Exit(m.Run()) }
TestMain 是一个低级原语,对于临时测试需求来说不是必需的,普通测试函数就足够了。
5 组织 testing 包
go的testing包为了给自动化测试提供支持。
它旨在与“go test”命令一起使用,该命令自动执行表单的任何功能和基准测试。
其中 Xxx 不以小写字母开头。函数名称用于标识测试例程。
在这些功能中,使用 Error、Fail 或相关方法来指示失败。
要编写新的测试套件,请创建一个名称以 _ test.go 结尾的文件,其中包含此处所述的 TestXxx 函数。
将文件放在与被测文件相同的包中。
该文件将从常规包构建中排除,但会在运行“go test”命令时包含在内。
有关更多详细信息,请运行“go help test”和“go help testflag”。
一个简单的测试函数如下所示:
func TestAbs(t * testing.T) {
hava := Abs(-1)
if have != 1 {
t.Errorf("Abs(-1) = %d; need 1", hava)
}
}
由于go 的特征,它的测试一般封装为 模块 demo_test.go --> 函数 Context, 一些常用的特殊的测试关键字 如下”
测试函数
TestXxxx()
测试函数的 套件设置和销毁
Setup()
TestCase()
TearDown()
测试套件的 设置和销毁
Setup()
TestSuite()
TearDown()
包级别的 设置和销毁
pkgSetup()
pkgTearDown()
支持回收测试套件设置 对于 包级别的测试固件的创建和销毁有了正式的原生支持 >go 1.14
TestMain()
6 使用方式:
运行全部测试
go test -run ''
运行匹配“Foo”的顶级测试,例如“TestFooBar”。
go test -run Foo
对于匹配“Foo”的顶级测试,运行匹配“A=”的子测试。
go test -run Foo/A=
对于所有顶级测试,运行匹配“A=1”的子测试。
go test -run /A=1
模糊匹配“FuzzFoo”的目标
go test -fuzz FuzzFoo
** 例子:
设置测试套件
func suiteSetup(suiteName string) func() {
fmt.Printf("\tsetUp fixture for suite %v \n", suiteName)
return func() {
fmt.Printf("\ttearDown fixture for suite %v\n", suiteName)
}
}
设置测试用例
func funcOneTestCase(t * testing.T) {
fmt.Printf("\t\tExecute test:%v\n", t.Name())
}
func TestFuncOne(t * testing.T) {
t.CleanUp(suiteSetUp(t.Name()))
t.Run("testcase1", funcOneTestCase1)
}
用例的函数
func funcTwoTestCase(t * testing.T) {
fmt.Printf("\t\tExecute test:%v\n", t.Name())
}
执行清理和用例
func TestFuncTwo(t * testing.T) {
t.CleanUp(suiteSetUp(t.Name()))
t.Run("testcase1", funcTwoTestCase1)
}
包初始化
func pkgSetUp(pkgName string) func() {
fmt.Printf("package SetUp fixture for %v\n", pkgName)
return func() {
fmt.Printf("package TearDown fixture for %v\n", pkgName)
}
}
控制哪些代码在主例程上运行
func TestMain(m * testing.M) {
defer pkgSetUp("package demo_test")()
m.Run()
}
7 小结
go内置的 testing 包,其测试方式为:
1 测试用例封装在普通go函数中, 2 针对给定的输入数据,比较被测试函数,方法返回的实际结果值和预期值, 若有差异,则通过testing 包提供的函数输出差异信息。 3 失败断定为进入 Error/Errorf(退出当前用例), Fatal/Fatalf(立刻退出测试主进程), 当函数进入 这些地方将被记录在testing的输出报告中。 4 go 内置了不少特别的测试方式,如性能基准,输出接口,模糊测试,关键测试等等。
更多信息: