README ¶
Chapter 7 测试
目前go test支持的测试类型有:
- 单元测试
- 性能测试
- 示例测试
7.1 快速开始
1.单元测试
单元测试是指对软件中的最小测试单元进行检测和验证,比如对一个函数的测试。
2.性能测试
性能测试也成为基准测试,可以测试一段程序的性能,可以得到时间消耗、内存使用情况的报告。
3.示例测试
广泛应用于Go源码和各种开源框架中,用于展示某个包或某个方法的用法。
比如,Go标准库中,mail包展示如何从一个字符串解析出邮件列表的用法,非常直观易懂。
源码位于 src/net/mail/example_test.go 中:
func ExampleParseAddressList() {
const list = "Alice <alice@example.com>, Bob <bob@example.com>, Eve <eve@example.com>"
emails, err := mail.ParseAddressList(list)
if err != nil {
log.Fatal(err)
}
for _, v := range emails {
fmt.Println(v.Name, v.Address)
}
// Output:
// Alice alice@example.com
// Bob bob@example.com
// Eve eve@example.com
}
总结
1. 单元测试
通过package语句可以看到,测试文件属于“gotest_test”包,测试文件也可以跟源文件在同一个包,但常见的做法是创建一个包专用于测试, 这样可以使测试文件和源文件隔离。GO源代码以及其他知名的开源框架通常会创建测试包,而且规则是在原包名上加上”_test”。
测试函数命名规则为”TestXxx”,其中“Test”为单元测试的固定开头,go test只会执行以此为开头的方法。 紧跟“Test”是以首字母大写的单词,用于识别待测试函数。
测试函数参数并不是必须要使用的,但”testing.T”提供了丰富的方法帮助控制测试流程。
t.Errorf()用于标记测试失败,标记失败还有几个方法,在介绍testing.T结构时再详细介绍。
go test
2. 性能测试
性能测试函数命名规则为”BenchmarkXxx”,其中”Xxx”为自定义的标识,需要以大写字母开始,通常为待测函数。
testing.B提供了一系列的用于辅助性能测试的方法或成员,比如本例中的 b.N 表示循环执行的次数,而N值不用程序员特别关心,按照官方说法, N值是动态调整的,直到可靠的算出程序执行时间后才会停止,具体执行次数会在执行结束后打印出来。
其中 -bench 为go test的flag,该flag指示go test进行性能测试,即执行测试文件中符合”BenchmarkXxx”规 则的方法。 紧跟flag的为flag的参数,本例表示执行当前所有的性能测试。
go test -bench=.
3. 示例测试
- 例子测试函数名需要以”Example”开头;
- 检测单行输出格式为“// Output: <期望字符串>”;
- 检测多行输出格式为“// Output: \ <期望字符串> \ <期望字符串>”,每个期望字符串占一行;
- 检测无序输出格式为”// Unordered output: \ <期望字符串> \ <期望字符串>”,每个期望字符串占一 行;
- 测试字符串时会自动忽略字符串前后的空白字符;
- 如果测试函数中没有“Output”标识,则该测试函数不会被执行;
- 执行测试可以使用 go test ,此时该目录下的其他测试文件也会一并执行;
- 执行测试可以使用 go test <xxx_test.go> ,此时仅执行特定文件中的测试函数;
go test example_test.go
7.2 进阶测试
7.2.1 子测试
简单的说,子测试提供一种在一个测试函数中执行多个测试的能力,比如原来有TestA、TestB和TestC三个测试函数, 每个测试函数执行开始都需要做些相同的初始化工作,那么可以利用子测试将这三个测试合并到一个测试中, 这样初始化工作只需要做一次。 除此之外,子测试还提供了诸多便利,下面我们逐一说明。
go test subunit_test.go -v
子测试命名规则
通过上面的例子我们知道 Run() 方法第一个参数为子测试的名字,而实际上子测试的内部命名规则为:”<父测试名 字>/<传递给Run的名字>“。 比如,传递给 Run() 的名字是“A=1”,那么子测试名字为“TestSub/A=1”。这个在上面的命令行输出中也可以看出。
过滤筛选
通过测试的名字,可以在执行中过滤掉一部分测试。 比如,只执行上例中“A=*”的子测试,那么执行时使用 -run Sub/A= 参数即可;
子测试并发
前面提到的多个子测试共享setup和teardown有一个前提是子测试没有并发,如果子测试使用 t.Parallel() 指定并发,那么就没办法共享teardown了, 因为执行顺序很可能是setup->子测试1->teardown->子测试2…。 如果子测试可能并发,则可以把子测试通过 Run() 再嵌套一层,Run() 可以保证其下的所有子测试执行结束后再返回。
go test subparallel_test.go -v -run SubParallel
总结
- 子测试适用于单元测试和性能测试;
- 子测试可以控制并发;
- 子测试提供一种类似table-driven风格的测试;
- 子测试可以共享setup和tear-down;
注释:setup 和 teardown 是在每个 case 执行前后都需要执行的操作
7.2.1 Main测试
我们知道子测试的一个方便之处在于可以让多个测试共享Setup和Tear-down。但这种程度的共享有时并不满足需求, 有时希望在整个测试程序做一些全局的setup和Tear-down,这时就需要Main测试了。
所谓Main测试,即声明一个 func TestMain(m *testing.M) ,它是名字比较特殊的测试,参数类型为 testing.M 指针。 如果声明了这样一个函数,当前测试程序将不是直接执行各项测试,而是将测试交给TestMain调度。
7.3 实现原理
7.3.1 testing.common
我们知道单元测试函数需要传递一个 testing.T 类型的参数,而性能测试函数需要传递一个 testing.B 类型的参 数,该参数可用于控制测试的流程, 比如标记测试失败等。
testing.T 和 testing.B 属于 testing 包中的两个数据类型,该类型提供一系列的方法用于控制函数执行流程, 考虑到二者有一定的相似性, 所以Go实现时抽象出一个 testing.common 作为一个基础类型, 而 testing.T 和 testing.B 则属于 testing.common 的扩展。
common.helpers 已经被取消了:https://github.com/golang/go/commit/4c174a7ba66724f8f9a1915c8f4868a8b3aaf219
7.3.2 testing.TB接口
TB接口,顾名思义,是testing.T(单元测试)和testing.B(性能测试)共用的接口。
TB接口通过在接口中定义一个名为private()的私有方法,保证了即使用户实现了类似的接口,也不会跟 testing.TB接口冲突。
其实,这些接口在testing.T和testing.B公共成员testing.common中已经实现。
7.3.3 单元测试实现原理
简介 在了解过testing.common后,我们进一步了解testing.T数据结构,以便了解更多单元测试执行的更多细节。
数据结构 源码包src/testing/testing.go:T定义了其数据结构:
type T struct {
common
isParallel bool
context *testContext // For running tests and subtests.
}
其成员简单介绍如下:
common: 即前面绍的testing.common
isParallel: 表示当前测试是否需要并发,如果测试中执行了t.Parallel(),则此值为true
context: 控制测试的并发调度
因为context直接决定了单元测试的调度,在介绍testing.T支持的方法前,有必要先了解一下context。
7.3.4 性能测试实现原理
简介
跟据前面章节,我们可以快速的写出一个性能测试并执行,最令我感到神奇的是b.N的值,虽然官方资料中说b.N会自动调整以保证可靠的计时, 可还是想了解具体的实现机制。
本节,我们先分析testing.B数据结构,再看几个典型的成员函数,以期从源码中寻找以下问题的答案:
- b.N是如何自动调整的?
- 内存统计是如何实现的?
- SetBytes()其使用场景是什么?
数据结构
源码包src/testing/benchmark.go:B定义了性能测试的数据结构,我们提取其比较重要的一些成员进行分析:
type B struct {
common
importPath string // import path of the package containing the benchmark
context *benchContext
N int
previousN int // number of iterations in the previous run
previousDuration time.Duration // total duration of the previous run
benchFunc func(b *B)
benchTime benchTimeFlag
bytes int64
missingBytes bool // one of the subbenchmarks does not have bytes set.
timerOn bool
showAllocResult bool
result BenchmarkResult
parallelism int // RunParallel creates parallelism*GOMAXPROCS goroutines
// The initial states of memStats.Mallocs and memStats.TotalAlloc.
startAllocs uint64
startBytes uint64
// The net total of this test after being run.
netAllocs uint64
netBytes uint64
// Extra metrics collected by ReportMetric.
extra map[string]float64
}
其主要成员如下:
- common: 与testing.T共享的testing.common,管理着日志、状态等;
- N:每个测试中用户代码执行次数
- benchFunc:测试函数
- benchTime:性能测试最少执行时间,默认为1s,可以通过能数-benchtime 2s指定
- bytes:每次迭代处理的字节数
- timerOn:计时启动标志,默认为false,启动计时为true
- startAllocs:测试启动时记录堆中分配的对象数
- startBytes:测试启动时记录堆中分配的字节数
- netAllocs:测试结束后记录堆中新增加的对象数,公式:结束时堆中分配的对象数
- netBytes:测试对事后记录堆中新增加的字节数
设置处理字节数:B.SetBytes(n int64)
// SetBytes records the number of bytes processed in a single operation.
// If this is called, the benchmark will report ns/op and MB/s.
func (b *B) SetBytes(n int64) { b.bytes = n }
这是一个比较含糊的函数,通过其函数说明很难明白其作用。
其实它是用来设置单次迭代处理的字节数,一旦设置了这个字节数, 那么输出报告中将会呈现“xxx MB/s”的信息, 用来表示待测函数处理字节的性能。 待测函数每次处理多少字节数只有用户清楚,所以需要用户设置。
举个例子,待测函数每次执行处理1M数据,如果我们想看待测函数处理数据的性能,那么我们在测试中设置 SetByte(1024 *1024), 假如待测函数需要执行1s的话,那么结果中将会出现 “1 MB/s”(约等于)的信息。示例代码如下所示:
func BenchmarkSetBytes(b *testing.B) {
b.SetBytes(1024 * 1024)
for i := 0; i < b.N; i++ {
time.Sleep(1 * time.Second) // 模拟待测函数
}
}
打印结果:
goos: darwin
goarch: amd64
pkg: github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkSetBytes
BenchmarkSetBytes-8 1 1004217211 ns/op 1.04 MB/s
PASS
可以看到测试执行了一次,花费时间约1S,数据处理能力约为1MB/s。
B.N是如何调整的?
B.launch()方法里最终决定B.N的值。我们看下伪代码:
func (b *B) launch() { // 此方法自动测算执行次数,但调用前必须调用run1以便自动计算次数
d := b.benchTime
for n := 1; !b.failed && b.duration < d && n < 1e9; { // 最少执行b.benchTime(默认为1s)时间,最多执行1e9次
last := n
n = int(d.Nanoseconds()) // 预测接下来要执行多少次,b.benchTime/每个操作耗时
if nsop := b.nsPerOp(); nsop != 0 {
n /= int(nsop)
}
n = max(min(n+n/5, 100*last), last+1) // 避免增长较快,先增长20%,至少增长1次
n = roundUp(n) // 下次迭代次数向上取整到10的指数,方便阅读
b.runN(n)
}
}
不考虑程序出错,而且用户没有主动停止测试的场景下,每个性能测试至少要执行b.benchTime长的秒数,默认为1s。 先执行一遍的意义在于看用户代码执行一次要花费多长时间,如果时间较短,那么b.N值要足够大才可以测得更精确,如果时间较长,b.N值相应的会减少, 否则会影响测试效率。
最终的b.N会被定格在某个10的指数级,是为了方便阅读测试报告。
内存是如何统计的?
我们知道在测试开始时,会把当前内存值记入到b.startAllocs和b.startBytes中,测试结束时,会用最终内存值与开始时的内存值相减, 得到净增加的内存值,并记入到b.netAllocs和b.netBytes中。
每个测试结束,会把结果保存到BenchmarkResult对象里,该对象里保存了输出报告所必需的统计信息:
// BenchmarkResult contains the results of a benchmark run.
type BenchmarkResult struct {
N int // The number of iterations.
T time.Duration // The total time taken.
Bytes int64 // Bytes processed in one iteration.
MemAllocs uint64 // The total number of memory allocations.
MemBytes uint64 // The total number of bytes allocated.
// Extra records additional metrics reported by ReportMetric.
Extra map[string]float64
}
其中MemAllocs和MemBytes分别对应b.netAllocs和b.netBytes。
那么最终统计时只需要把净增加值除以b.N即可得到每次新增多少内存了。
每个操作内存对象新增值:
// AllocsPerOp returns the "allocs/op" metric,
// which is calculated as r.MemAllocs / r.N.
func (r BenchmarkResult) AllocsPerOp() int64 {
if v, ok := r.Extra["allocs/op"]; ok {
return int64(v)
}
if r.N <= 0 {
return 0
}
return int64(r.MemAllocs) / int64(r.N)
}
每个操作内存字节数新增值:
// AllocedBytesPerOp returns the "B/op" metric,
// which is calculated as r.MemBytes / r.N.
func (r BenchmarkResult) AllocedBytesPerOp() int64 {
if v, ok := r.Extra["B/op"]; ok {
return int64(v)
}
if r.N <= 0 {
return 0
}
return int64(r.MemBytes) / int64(r.N)
}
7.3.5 示例测试实现原理
简介 示例测试相对于单元测试和性能测试来说,其实现机制比较简单。它没有复杂的数据结构,也不需要额外的流程控制,其核心工作原理在于收集测试过程中的打印日志,然后与期望字符串做比较,最后得出是否一致的报告。
testing/example.go
数据结构
type InternalExample struct {
Name string // 测试名称
F func() // 测试函数
Output string // 期望字符串
Unordered bool // 输出是否是无序的
}
比如,示例测试如下:
// 检测乱序输出
func ExamplePrintNames() {
gotest.PrintNames()
// Unordered output:
// Jim
// Bob
// Tom
// Sue
}
该示例测试经过编译后,产生的数据结构成员如下:
- InternalExample.Name = "ExamplePrintNames";
- InternalExample.F = ExamplePrintNames()
- InternalExample.Output = "Jim\n Bob\n Tom\n Sue\n"
- InternalExample.Unordered = true; 其中Output是包含换行符的字符串。
7.3.6 Main测试的实现原理
简介
每一种测试(单元测试、性能测试或示例测试),都有一个数据类型与其对应。
单元测试:InternalTest
性能测试:InternalBenchmark
示例测试:InternalExample
测试编译阶段,每个测试都会被放到指定类型的切片中,测试执行时,这些测试将会被放到testing.M数据结构中进行调度。
而testing.M即是MainTest对应的数据结构。
数据结构
源码src\testing/testing.go:M定义了testing.M的数据结构:
// M is a type passed to a TestMain function to run the actual tests.
type M struct {
deps testDeps
tests []InternalTest
benchmarks []InternalBenchmark
examples []InternalExample
timer *time.Timer
afterOnce sync.Once
numRun int
// value to pass to os.Exit, the outer test func main
// harness calls os.Exit with this code. See #34129.
exitCode int
}
单元测试、性能测试和示例测试在经过编译后都会被存放到一个testing.M数据结构中,在测试执行时该数据结构将传递给TestMain(), 真正执行测试的是testing.M的Run()方法,这个后面我们会继续分析。
timer用于指定测试的超时时间,可以通过参数timeout 指定,当测试执行超时后将会立即结束并判定为失败。
执行测试
TestMain()函数通常会有一个m.Run()方法,该方法会执行单元测试、性能测试和示例测试,如果用户实现了TestMain()但没有调用m.Run()的话, 那么什么测试都不会被执行。
m.Run()不仅会执行测试,还会做一些初始化工作,比如解析参数、起动定时器、跟据参数指示创建一系列的文件等。
m.Run()使用三个独立的方法来执行三种测试:
- 单元测试:runTests(m.deps.MatchString, m.tests)
- 性能测试:runExamples(m.deps.MatchString, m.examples)
- 示例测试:runBenchmarks(m.deps.ImportPath(), m.deps.MatchString, m.benchmarks) 其中m.deps里存放了测试匹配相关的内容,暂时先不用关注。
7.3.7 go test的工作机制
前言
前面的章节我们分析了每种测试的数据结构及其实现原理,本节我们看一下go test的执行机制。
Go 有多个命令行工具,go test只是其中一个。go test命令的函数入口在src\cmd\go\internal\test\test.go:runTest(), 这个函数就是go test的大脑。
两种运行模式
go test运行时,跟据是否指定package分为两种模式,即本地目录模式和包列表模式。
本地目录模式
当执行测试并没有指定package时,即以本地目录模式运行,例如使用"go test"或者"go test -v"来启动测试。
本地目录模式下,go test编译当前目录的源码文件和测试文件,并生成一个二进制文件,最后执行并打印结果。
包列表模式
当执行测试并显式指定package时,即以包列表模式运行,例如使用"go test math"来启动测试。
包列表模式下,go test为每个包生成一个测试二进制文件,并分别执行它。 包列表模式是在Go 1.10版本才引入的,它会把每个包的测试结果写入到本地临时 文件中做为缓存,下次执行时会直接从缓存中读取测试结果,以便节省测试时间。
缓存机制 当满足一定的条件,测试的缓存是自动启用的,也可以显式的关闭缓存。
测试结果缓存 如果一次测试中,其参数全部来自"可缓存参数"集合,那么本次测试结果将被缓存。
可缓存参数集合如下:
- -cpu
- -list
- -parallel
- -run
- -short
- -v
需要注意的是,测试参数必须全部来自这个集合,其结果才会被缓存,没有参数或包含任一此集合之外的参数,结果都不会缓存。
使用缓存结果
如果满足条件,测试不会真正执行,而是从缓存中取出结果并呈现,结果中会有"cached"字样,表示来自缓存。
使用缓存结果也需要满足一定的条件:
- 本次测试的二进制及测试参数与之前的一次完全一致;
- 本次测试的源文件及环境变量与之前的一次完全一致;
- 之前的一次测试结果是成功的;
- 本次测试运行模式是列表模式;
下面演示一个使用缓存的例子:
go test .
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest 1.273s
go test .
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest (cached)
前后两次执行测试,参数没变,源文件也没变化,第二次执行时会自动从缓存中获取结果,结果中“cached”即表示结果从缓存中获取。
禁用缓存
测试时使用一个不在“可缓存参数”集合中的参数,就不会使用缓存,比较常用的方法是指定一个参数“-count=1”。
下面演示一个禁用缓存的例子:
go test .
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest 1.273s
go test .
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest (cached)
go test . -count=1
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest 0.565s
第三次执行使用了参数"-count=1",所以执行时不会从缓存中获取结果。
7.4 扩展阅读
go test非常容易上手,但并不代表其功能单一,它提供了丰富的参数接口以便满足各种测试场景。
本节,我们主要介绍一些常用的参数,通过前面实现原理的学习和本节的示例,希望读者可以准确掌握其用法,以便在工作中提供便利。
7.4.1 测试参数
前言
go test有非常丰富的参数,一些参数用于控制测试的编译,另一些参数控制测试的执行。
有关测试覆盖率、vet和pprof相关的参数先略过,我们在讨论相关内容时再详细介绍。
1.控制编译的参数
1) -args
指示go test把-args后面的参数带到测试中去。具体的测试函数会跟据此参数来控制测试流程。
-args后面可以附带多个参数,所有参数都将以字符串形式传入,每个参数做为一个string,并存放到字符串切片中。
// TestArgs 用于演示如何解析-args参数
func TestArgs(t *testing.T) {
if !flag.Parsed() {
flag.Parse()
}
argList := flag.Args() // flag.Args() 返回 -args 后面的所有参数,以切片表示,每个元素代表一个参数
for _, arg := range argList {
if arg == "cloud" {
t.Log("Running in cloud.")
}else {
t.Log("Running in other mode.")
}
}
}
执行测试时带入参数:
go test . -run TestArgs -v -args "cloud"
=== RUN TestArgs
testargs_test.go:16: Running in cloud.
--- PASS: TestArgs (0.00s)
PASS
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest 3.054s
通过参数-args指定传递给测试的参数。
2) json
-json 参数用于指示go test将结果输出转换成json格式,以方便自动化测试解析使用。
示例如下:
go test -run TestAdd -json
{"Time":"2021-12-07T15:22:07.760538+08:00","Action":"run","Package":"github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest","Test":"TestAdd"}
{"Time":"2021-12-07T15:22:07.760926+08:00","Action":"output","Package":"github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest","Test":"TestAdd","Output":"=== RUN TestAdd\n"}
{"Time":"2021-12-07T15:22:07.760973+08:00","Action":"output","Package":"github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest","Test":"TestAdd","Output":"--- PASS: TestAdd (0.00s)\n"}
{"Time":"2021-12-07T15:22:07.760992+08:00","Action":"pass","Package":"github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest","Test":"TestAdd","Elapsed":0}
{"Time":"2021-12-07T15:22:07.761019+08:00","Action":"output","Package":"github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest","Output":"PASS\n"}
{"Time":"2021-12-07T15:22:07.761075+08:00","Action":"output","Package":"github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest","Output":"ok \tgithub.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest\t1.034s\n"}
{"Time":"2021-12-07T15:22:07.761104+08:00","Action":"pass","Package":"github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest","Elapsed":1.035}
3)-o
-o 参数指定生成的二进制可执行程序,并执行测试,测试结束不会删除该程序。
没有此参数时,go test生成的二进制可执行程序存放到临时目录,执行结束便删除。
示例如下:
go test -run TestAdd -o TestAdd
PASS
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest 3.078s
本例中,使用-o 参数指定生成二进制文件"TestAdd"并存放到当前目录,测试执行结束后,仍然可以直接执行该二进制程序。
2.控制测试的参数
1) -bench regexp
go test默认不执行性能测试,使用-bench参数才可以运行,而且只运行性能测试函数。
其中正则表达式用于筛选所要执行的性能测试。如果要执行所有的性能测试,使用参数"-bench ."或"-bench=."。
此处的正则表达式不是严格意义上的正则,而是种包含关系。
比如有如下三个性能测试:
- func BenchmarkMakeSliceWithoutAlloc(b *testing.B)
- func BenchmarkMakeSliceWithPreAlloc(b *testing.B)
- func BenchmarkSetBytes(b *testing.B) 使用参数“-bench=Slice”,那么前两个测试因为都包含"Slice",所以都会被执行,第三个测试则不会执行。
对于包含子测试的场景下,匹配是按层匹配的。举一个包含子测试的例子:
func BenchmarkSub(b *testing.B) {
b.Run("A=1", benchSub1)
b.Run("A=2", benchSub2)
b.Run("B=1", benchSub3)
}
测试函数命名规则中,子测试的名字需要以父测试名字做为前缀并以"/"连接,上面的例子实际上是包含4个测试:
- Sub
- Sub/A=1
- Sub/A=2
- Sub/B=1
如果想执行三个子测试,那么使用参数“-bench Sub”。如果只想执行“Sub/A=1”,则使用参数"-bench Sub/A=1"。如果想执行"Sub/A=1"和“Sub/A=2”, 则使用参数"-bench Sub/A="。
2) -benchtime s
-benchtime指定每个性能测试的执行时间,如果不指定,则使用默认时间1s。
例如,执定每个性能测试执行2s,则参数为:"go test -bench Sub/A=1 -benchtime 2s"。
3) -cpu
参数提供一个CPU个数的列表,提供此列表后,那么测试将按照这个列表指定的CPU数设置GOMAXPROCS并分别测试。
比如“-cpu 1,2”,那么每个测试将执行两次,一次是用1个CPU执行,一次是用2个CPU执行。 例如,使用命令"go test -bench Sub/A=1 -cpu 1,2,3,4" 执行测试:
go test -bench Sub/A=1 -cpu 1,2,3,4
goos: darwin
goarch: amd64
pkg: github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkSub/A=1 2317 497982 ns/op
BenchmarkSub/A=1-2 2617 437995 ns/op
BenchmarkSub/A=1-3 2557 457854 ns/op
BenchmarkSub/A=1-4 2491 489951 ns/op
PASS
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start 5.239s
测试结果中测试名后面的-2、-3、-4分别代表执行时GOMAXPROCS的数值。 如果GOMAXPROCS为1,则不显示。
4) -count n
-count指定每个测试执行的次数,默认执行一次。
例如,指定测试执行2次:
go test -bench Sub/A=1 -count 2
goos: darwin
goarch: amd64
pkg: github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkSub/A=1-8 2233 508007 ns/op
BenchmarkSub/A=1-8 2142 538325 ns/op
PASS
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start 3.079s
可以看到结果中也将呈现两次的测试结果。
如果使用-count指定执行次数的同时还指定了-cpu列表,那么测试将在每种CPU数量下执行count指定的次数。
注意,示例测试不关心-count和-cpu参数,它总是执行一次。
5) -failfast
默认情况下,go test将会执行所有匹配到的测试,并最后打印测试结果,无论成功或失败。
-failfast指定如果有测试出现失败,则立即停止测试。这在有大量的测试需要执行时,能够更快的发现问题。
6) -list regexp
-list 只是列出匹配成功的测试函数,并不真正执行。而且,不会列出子函数。
例如,使用参数"-list Sub"则只会列出包含子测试的三个测试,但不会列出子测试:
~/project/golang/go-daily-lib/expert_programming/chapter7/7.1_quick_start$ go test -list Sub
BenchmarkSub
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start 1.090s
7) -parallel n
指定测试的最大并发数。
当测试使用t.Parallel()方法将测试转为并发时,将受到最大并发数的限制,默认情况下最多有GOMAXPROCS个测试并发,其他的测试只能阻塞等待。
8) -run regexp 跟据正则表达式执行单元测试和示例测试。正则匹配规则与-bench 类似。
9) -timeout d
默认情况下,测试执行超过10分钟就会超时而退出。
例时,我们把超时时间设置为1s,由本来需要3s的测试就会因超时而退出:
~/project/golang/go-daily-lib/expert_programming/chapter7/7.2_advanced_test/7.2.1sub$ go test -timeout=1s
panic: test timed out after 1s
goroutine 33 [running]:
testing.(*M).startAlarm.func1()
设置超时可以按秒、按分和按时:
按秒设置:-timeout xs或-timeout=xs
按分设置:-timeout xm或-timeout=xm
按时设置:-timeout xh或-timeout=xh
10) -v
默认情况下,测试结果只打印简单的测试结果,-v 参数可以打印详细的日志。
性能测试下,总是打印日志,因为日志有时会影响性能结果。
11) -benchmem
默认情况下,性能测试结果只打印运行次数、每个操作耗时。使用-benchmem则可以打印每个操作分配的字节数、每个操作分配的对象数。
// 没有使用-benchmem
~/project/golang/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkSetBytes-8 1 1003846510 ns/op 1.04 MB/s
BenchmarkMakeSliceWithoutAlloc-8 1982 520529 ns/op
BenchmarkMakeSliceWithPreAlloc-8 7324 154560 ns/op
PASS
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest 4.128s
// 使用-benchmem
go test -bench=. -benchmem
goos: darwin
goarch: amd64
pkg: github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkSetBytes-8 1 1005064363 ns/op 1.04 MB/s 112 B/op 4 allocs/op
BenchmarkMakeSliceWithoutAlloc-8 2032 493115 ns/op 4654348 B/op 30 allocs/op
BenchmarkMakeSliceWithPreAlloc-8 8691 141946 ns/op 802819 B/op 1 allocs/op
PASS
ok github.com/stevenlee87/go-daily-lib/expert_programming/chapter7/7.1_quick_start/gotest 3.493s
此处,每个操作的含义是放到循环中的操作,如下示例所示:
func BenchmarkMakeSliceWithoutAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
gotest.MakeSliceWithoutAlloc() // 一次操作
}
}
7.4.2 benchstat
Go官方推荐的性能测试分析工具benchstat
go get golang.org/x/perf/cmd/benchstat
go: downloading golang.org/x/perf v0.0.0-20211012211434-03971e389cd3
go get: added golang.org/x/perf v0.0.0-20211012211434-03971e389cd3
1) 分析一组样本
benchstat old.txt
2) 分析两组样本
benchstat old.txt new.txt