package
0.0.0-20240926033524-0989935589bb
Repository: https://github.com/stevenlee87/go-daily-lib.git
Documentation: pkg.go.dev

# Packages

No description provided by the author
No description provided by the author

# README

Chapter 7 测试

目前go test支持的测试类型有:

  • 单元测试
  • 性能测试
  • 示例测试

7.1 快速开始

1.单元测试

单元测试是指对软件中的最小测试单元进行检测和验证,比如对一个函数的测试。

2.性能测试

性能测试也成为基准测试,可以测试一段程序的性能,可以得到时间消耗、内存使用情况的报告。

3.示例测试

广泛应用于Go源码和各种开源框架中,用于展示某个包或某个方法的用法。
比如,Go标准库中,mail包展示如何从一个字符串解析出邮件列表的用法,非常直观易懂。
源码位于 src/net/mail/example_test.go 中:

func ExampleParseAddressList() {
    const list = "Alice <[email protected]>, Bob <[email protected]>, Eve <[email protected]>"
    emails, err := mail.ParseAddressList(list)
    if err != nil {
        log.Fatal(err)
    }

    for _, v := range emails {
        fmt.Println(v.Name, v.Address)
    }

    // Output:
    // Alice [email protected]
    // Bob [email protected]
    // Eve [email protected]
}

总结

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. 示例测试

  1. 例子测试函数名需要以”Example”开头;
  2. 检测单行输出格式为“// Output: <期望字符串>”;
  3. 检测多行输出格式为“// Output: \ <期望字符串> \ <期望字符串>”,每个期望字符串占一行;
  4. 检测无序输出格式为”// Unordered output: \ <期望字符串> \ <期望字符串>”,每个期望字符串占一 行;
  5. 测试字符串时会自动忽略字符串前后的空白字符;
  6. 如果测试函数中没有“Output”标识,则该测试函数不会被执行;
  7. 执行测试可以使用 go test ,此时该目录下的其他测试文件也会一并执行;
  8. 执行测试可以使用 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