GoalngTest完全攻略

一、提示

go test 命令,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件。

输出的信息类似下面所示的样子:

3.go测试的操作 go test -v
=== RUN   Test_str
    go_test.go:9: 测试1
--- PASS: Test_str (0.00s)
PASS
ok      MyTest  0.432s

性能测试系统可以给出代码的性能数据,帮助测试者分析性能问题。
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java 里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。

单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。


二、单元测试

要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时需要让文件必须以_test结尾。默认的情况下,go test命令不需要任何的参数,它会自动把你源码包下面所有 test 文件测试完毕,当然你也可以带上参数。

这里介绍几个常用的参数:

  • -bench regexp 执行相应的 benchmarks,例如 -bench=.;
  • -cover 开启测试覆盖率;
  • -run regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数;
  • -v 显示测试的详细命令。

单元测试源码文件可以由多个测试用例组成,每个测试用例函数需要以Test为前缀,例如:

func Test_str(t *testing.T)
  • 测试用例文件不会参与正常源码编译,不会被包含到可执行文件中。
  • 测试用例文件使用go test指令来执行,没有也不需要 main() 作为函数入口。所有在以_test结尾的源码内以Test开头的函数会自动被执行。
  • 测试用例可以不传入 *testing.T 参数。

测试代码示例:

package MyTest

import (
	"testing"
)

func Test_str(t *testing.T) {  // 单元测试函数必须以 Test 开始,参数为 *testing.T 的函数。
	t.Log("测试1")  // 使用 testing 包的 T 结构提供的 Log() 方法打印字符串
}


2.1、单元测试命令行

单元测试使用 go test 命令启动,例如:

3.go测试的操作 git:(main) go test unit_test.go  // 
ok      command-line-arguments  0.447s

第一行:在 go test 后跟 go_test.go 文件,表示测试这个文件里的所有测试用例

第二行:显示测试结果,ok 表示测试通过,command-line-arguments 是测试用例需要用到的一个包名,0.003s 表示测试花费的时间

3.go测试的操作 git:(main) go test -v unit_test.go
=== RUN   Test_str
    go_test.go:9: 测试1
--- PASS: Test_str (0.00s)
PASS
ok      command-line-arguments  0.081s

第一行:显示在附加参数中添加了-v,可以让测试时显示详细的流程

第二行:表示开始运行名叫 Test_str 的测试用例

第三行:是测试代码的输出

第四行:表示已经运行完 Test_str 的测试用例,PASS 表示测试成功


2.2、运行指定单元测试用例

go test指定文件时默认执行文件内的所有测试用例。可以使用-run参数选择需要的测试用例单独执行,参考下面的代码。

package MyTest

import (
	"testing"
)


func Test_A(t *testing.T) {
	t.Log("测试A")
}

func Test_B(t *testing.T) {
	t.Log("测试B")
}

这里指定 Test_A 进行测试:

3.go测试的操作 git:(main) ✗ go test -v unit_test.go -run Test_A
=== RUN   Test_A
    go_test.go:9: 测试A
--- PASS: Test_A (0.00s)
=== RUN   Test_A1
    go_test.go:13: 测试A1
--- PASS: Test_A1 (0.00s)
PASS
ok      command-line-arguments  0.428s

结果令人意外,虽然在命令中指定了只执行Test_A,但是结果却执了测试A , 测试A1, 这是因为 -run支持正则表达式,所以自动匹配走了有相同前缀的单元测试。可以使用-run TestA$来精确指定

3.go测试的操作 git:(main) ✗ go test -v -run Test_A$
=== RUN   Test_A
    go_test.go:9: 测试A
--- PASS: Test_A (0.00s)
PASS
ok      MyTest  0.579s

2.3、标记单元测试结果

当需要终止当前测试用例时,可以使用 FailNow,参考下面的代码

// 测试结果标记:需要终止当前测试用例
func TestFailNow(t *testing.T) {
	t.Log("TestFailNow before fail")  // 会执行

    t.FailNow()                     // 标记为失败

	t.Log("TestFailNow after fail")   // 不会执行
}

还有一种只标记错误不终止测试的方法,代码如下:

// 测试结果标记:只标记错误不终止测试
func TestFail(t *testing.T) {

    t.Log("TestFail before fail")  // 会执行

    t.Fail()                       // 标记为失败

    t.Log("TestFail after fail")   // 会执行
}

2.4、单元测试日志

每个测试用例可能并发执行,使用 testing.T 提供的日志输出可以保证日志跟随这个测试上下文一起打印输出。testing.T 提供了几种日志输出方法,详见下表所示。

方 法 备 注
Log 打印日志,同时结束测试
Logf 格式化打印日志,同时结束测试
Error 打印错误日志,同时结束测试
Errorf 格式化打印错误日志,同时结束测试
Fatal 打印致命日志,同时结束测试
Fatalf 格式化打印致命日志,同时结束测试

可以根据实际需要选择合适的日志。


三、基准测试

基准测试可以测试一段程序的运行性能及耗费 CPU 的程度。Go语言中提供了基准测试框架,使用方法类似于单元测试,使用者无须准备高精度的计时器和各种分析工具,基准测试本身即可以打印出非常标准的测试报告。

  1. 基准测试的代码文件必须以_test.go结尾
  2. 基准测试的函数必须以Benchmark开头,必须是可导出的
  3. 基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
  4. 基准测试函数不能有返回值
  5. b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰
  6. 最后的for循环很重要,被测试的代码要放到循环里
  7. b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能

3.1、基础测试基本使用

通过一个例子来了解基准测试的基本使用方法

使用 go mod init example 初始化一个模块,新增 fib.go 文件,实现函数 fib,用于计算第 N 个菲波那切数。

func Benchmark_Add(b *testing.B) {
    var n int
    for i := 0; i < b.N; i++ {
        n++
    }
}

接下来,在 fib_test.go 中实现一个 benchmark 用例:

package main

import "testing"

func BenchmarkFib(b *testing.B) {
	for n := 0; n < b.N; n++ {
		fib(30) // run fib(30) b.N times
	}
}

代码说明如下:

  • benchmark 和普通的单元测试用例一样,都位于 _test.go 文件中。
  • 函数名以 Benchmark 开头,参数是 b *testing.B。和普通的单元测试用例很像,单元测试函数名以 Test 开头,参数是 t *testing.T

go test 命令默认不运行 benchmark 用例的,如果想运行 benchmark 用例,则需要加上 -bench 参数。

例如:

➜  benchmark_test git:(main) ✗ go test -v -bench=.                   
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib
BenchmarkFib-8               318           3790867 ns/op
PASS
ok      example 2.734s

-bench 参数支持传入一个正则表达式,匹配到的用例才会得到执行

例如: 只运行以 Fib 结尾的

➜  benchmark_test git:(main) ✗ go test -v -bench='Fib$' .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib
BenchmarkFib-8               315           3812223 ns/op
PASS
ok      example 1.917s

注意:Windows 下使用 go test 命令行时,-bench=.应写为-bench="."


3.2、基准测试的原理

benchmark 用例的参数 b *testing.B,有个属性 b.N 表示这个用例需要运行的次数。b.N 对于每个用例都是不一样的。

那这个值是如何决定的呢?b.N 从 1 开始,如果该用例能够在 1s 内完成,b.N 的值便会增加,再次执行。b.N 的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快。

举个例子,更加容易理解基准测试的原理:

这个例子中每调用一次就会打印一次 b.N的值 [注意:-benchtime 用于设定测试时间,下面有讲述其用法]

func BenchmarkFff(b *testing.B) {
	for n := 0; n < b.N; n++ {
        // 停顿1秒
		time.Sleep(1 * time.Second)
	}
	b.Log("调用了一次", b.N)
}

运行结果:

➜  benchmark_test git:(main) ✗ go test -v -bench='Fff$' . -benchtime=10s .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFff
    fib_test.go:26: 调用了一次 1
    fib_test.go:26: 调用了一次 10
BenchmarkFff-8                10        1000674900 ns/op
PASS
ok      example 12.130s

从结果来看,BenchmarkFff 一共被调用了两次,第一次b.N的值为 1, 第二次为10,这个例子充分的说明了Benchmark的执行原理。b.N的值从1开始,如果BenchmarkFff函数可以在10苗内执行完成,那么b.N的值就会递增,这个例子中b.N递增为10,然后继续执行BenchmarkFff函数,发现BenchmarkFff函数的执行时间超出了限定的10秒,那么测试就结束了。

再次运行,时间调整为100秒:

➜  benchmark_test git:(main) ✗ go test -v -bench='Fff$' . -benchtime=100s .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFff
    fib_test.go:26: 调用了一次 1
    fib_test.go:26: 调用了一次 100
BenchmarkFff-8               100        1000862939 ns/op
PASS
ok      example 101.285s

3.3、自定义CPU核数

Benchmark_Add-8 中的 -8GOMAXPROCS,默认等于 CPU 核数。可以通过 -cpu 参数改变 GOMAXPROCS-cpu 支持传入一个列表作为参数,例如:

➜  benchmark_test git:(main) ✗ go test -bench='Fib$' -cpu=2,4 .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-2               316           3798750 ns/op
BenchmarkFib-4               312           3818075 ns/op
PASS
ok      example 3.642s

在这个例子中,改变 CPU 的核数对结果几乎没有影响,因为这个 Benchmark_Add 的调用是串行的。

使用RunParallel进行并行测试,新增一个并行的benchmark:

func BenchmarkFoo(b *testing.B) {
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			fib(30) // run fib(30) b.N times
		}
	})
}

并行测试一下:

➜  benchmark_test git:(main) ✗ go test -bench='Foo$' -cpu=2,4 .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFoo-2               624           1944998 ns/op
BenchmarkFoo-4              1206           1002599 ns/op
PASS
ok      example 3.215s

可以看到多线程的性能差异。


3.4、提升测试准确度

3.4.1、自定义测试时间

对于性能测试来说,提升测试准确度的一个重要手段就是增加测试的次数。我们可以使用 -benchtime-count 两个参数达到这个目的

benchmark 的默认时间是 1s,那么可以使用 -benchtime 指定为 5s。例如:

➜  benchmark_test git:(main) ✗ go test -bench='Fib$' -benchtime=5s .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-8              1561           3774486 ns/op
PASS
ok      example 6.700s

这里发现实际执行的时间是 6.700s,比 benchtime 的 5s 要长,因为测试用例编译、执行、销毁等是需要时间的。

-benchtime 设置为 5s,用例执行次数也变成了原来的 5倍,每次函数调用时间仍为 0.6s,几乎没有变化。

3.4.2、自定义测试次数

-benchtime 的值除了是时间外,还可以是具体的次数。例如,执行 30 次可以用 -benchtime=30x

➜  benchmark_test git:(main) ✗ go test -bench='Fib$' -benchtime=50x .               
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-8                50           3813326 ns/op
--- BENCH: BenchmarkFib-8
    fib_test.go:15: 调用了一次 1
    fib_test.go:16: 总共执行了 1
    fib_test.go:15: 调用了一次 50
    fib_test.go:16: 总共执行了 51
PASS
ok      example 0.264s

调用 50 次 fib(30),仅花费了 0.264s。

3.4.3、自定义测试轮数

-count 参数可以用来设置 benchmark 的轮数。例如,进行 3 轮 benchmark。

➜  benchmark_test git:(main) ✗ go test -bench=Fib$ -count=2 .
goos: darwin
goarch: arm64
pkg: example
BenchmarkFib-8               310           3816544 ns/op
--- BENCH: BenchmarkFib-8
    fib_test.go:15: 调用了一次 1
    fib_test.go:16: 总共执行了 1
    fib_test.go:15: 调用了一次 100
    fib_test.go:16: 总共执行了 101
    fib_test.go:15: 调用了一次 310
    fib_test.go:16: 总共执行了 411
BenchmarkFib-8               315           3858069 ns/op
--- BENCH: BenchmarkFib-8
    fib_test.go:15: 调用了一次 1
    fib_test.go:16: 总共执行了 412
    fib_test.go:15: 调用了一次 100
    fib_test.go:16: 总共执行了 512
    fib_test.go:15: 调用了一次 315
    fib_test.go:16: 总共执行了 827
PASS
ok      example 3.608s

3.5、测试内存分配情况

-benchmem 参数可以度量内存分配的次数。内存分配次数也性能也是息息相关的,例如不合理的切片容量,将导致内存重新分配,带来不必要的开销。

在下面的例子中,generateWithCapgenerate 的作用是一致的,生成一组长度为 n 的随机序列。唯一的不同在于,generateWithCap 创建切片时,将切片的容量(capacity)设置为 n,这样切片就会一次性申请 n 个整数所需的内存。

// generate_test.go
package main

import (
	"math/rand"
	"testing"
	"time"
)

func generateWithCap(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0, n)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}

func generate(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}

func BenchmarkGenerateWithCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		generateWithCap(1000000)
	}
}

func BenchmarkGenerate(b *testing.B) {
	for n := 0; n < b.N; n++ {
		generate(1000000)
	}
}

运行该用例的结果是:

➜  benchmark_test git:(main) ✗ go test -bench=Generate .                 
goos: darwin
goarch: arm64
pkg: example
BenchmarkGenerateWithCap-8            76          14514814 ns/op
BenchmarkGenerate-8                   61          17938094 ns/op
PASS
ok      example 2.635s

可以看到生成 100w 个长度的切片,GenerateWithCap 的耗时比 Generate 少 20%。

可以使用 -benchmem 参数看到内存分配的情况:

➜  benchmark_test git:(main) ✗ go test -bench=Generate -benchmem .    
goos: darwin
goarch: arm64
pkg: example
BenchmarkGenerateWithCap-8            79          14566255 ns/op         8003700 B/op          1 allocs/op
BenchmarkGenerate-8                   64          17547603 ns/op        41678197 B/op         39 allocs/op
PASS
ok      example 2.381s

Generate 分配的内存是 GenerateWithCap 的 5 倍,设置了切片容量,内存只分配一次,而不设置切片容量,内存分配了 39 次。


3.6、测试不同的输入

不同的函数复杂度不同,O(1),O(n),O(n^2) 等,利用 benchmark 验证复杂度一个简单的方式,是构造不同的输入。对上面的 benchmark 稍作改造,便能够达到目的。

// generate_test.go
package main

import (
	"math/rand"
	"testing"
	"time"
)

func generate(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}
func benchmarkGenerate(i int, b *testing.B) {
	for n := 0; n < b.N; n++ {
		generate(i)
	}
}

func BenchmarkGenerate1000(b *testing.B)    { benchmarkGenerate(1000, b) }
func BenchmarkGenerate10000(b *testing.B)   { benchmarkGenerate(10000, b) }
func BenchmarkGenerate100000(b *testing.B)  { benchmarkGenerate(100000, b) }
func BenchmarkGenerate1000000(b *testing.B) { benchmarkGenerate(1000000, b) }

这里,实现了一个辅助函数 benchmarkGenerate 允许传入参数 i,并构造了 4 个不同输入的 benchmark 用例。运行结果如下:

➜  benchmark_test git:(main) ✗ go test generate2_test.go -bench=Generate .            
goos: darwin
goarch: arm64
BenchmarkGenerate1000-8            43047             28351 ns/op
BenchmarkGenerate10000-8            5773            184571 ns/op
BenchmarkGenerate100000-8            669           1855509 ns/op
BenchmarkGenerate1000000-8            62          17722528 ns/op
PASS
ok      command-line-arguments  5.638s

通过测试结果可以发现,输入变为原来的 10 倍,函数每次调用的时长也差不多是原来的 10 倍,这说明复杂度是线性的。


四、Benchmark 注意事项

3.1 ResetTimer

如果在 benchmark 开始前,需要一些准备工作,如果准备工作比较耗时,则需要将这部分代码的耗时忽略掉。比如下面的例子:

func BenchmarkFib(b *testing.B) {
	time.Sleep(time.Second * 3) // 模拟耗时准备任务
	for n := 0; n < b.N; n++ {
		fib(30) // run fib(30) b.N times
	}
}

运行结果是:

➜  benchmark_test git:(main) ✗ go test fib_test.go fib.go -bench=Fib$ -benchtime=50x
goos: darwin
goarch: arm64
BenchmarkFib-8                50          64149844 ns/op
--- BENCH: BenchmarkFib-8
    fib_test.go:16: 调用了一次 1
    fib_test.go:17: 总共执行了 1
    fib_test.go:16: 调用了一次 50
    fib_test.go:17: 总共执行了 51
PASS
ok      command-line-arguments  6.783s

50次调用,每次调用约 0.64s,是之前的 0.06s 的 11 倍。究其原因,受到了耗时准备任务的干扰。我们需要用 ResetTimer 屏蔽掉:

➜  benchmark_test git:(main) ✗ go test fib_test.go fib.go -bench=Fib$ -benchtime=50x
goos: darwin
goarch: arm64
BenchmarkFib-8                50           3932914 ns/op
--- BENCH: BenchmarkFib-8
    fib_test.go:17: 调用了一次 1
    fib_test.go:18: 总共执行了 1
    fib_test.go:17: 调用了一次 50
    fib_test.go:18: 总共执行了 51
PASS
ok      command-line-arguments  6.736s

运行结果恢复正常,每次调用约 0.03s。

3.2、StopTimer & StartTimer

还有一种情况,每次函数调用前后需要一些准备工作和清理工作,我们可以使用 StopTimer 暂停计时以及使用 StartTimer 开始计时。

例如,如果测试一个冒泡函数的性能,每次调用冒泡函数前,需要随机生成一个数字序列,这是非常耗时的操作,这种场景下,就需要使用 StopTimerStartTimer 避免将这部分时间计算在内。

例如:

// sort_test.go
package main

import (
	"math/rand"
	"testing"
	"time"
)

// generateWithCap 生成随机数切片,这里比较耗时,可以使用StopTimer & StartTimer来消除其干扰
func generateWithCap(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0, n)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}

// bubbleSort 冒泡排序
func bubbleSort(nums []int) {
	for i := 0; i < len(nums); i++ {
		for j := 1; j < len(nums)-i; j++ {
			if nums[j] < nums[j-1] {
				nums[j], nums[j-1] = nums[j-1], nums[j]
			}
		}
	}
}

func BenchmarkBubbleSort(b *testing.B) {
	for n := 0; n < b.N; n++ {
		b.StopTimer()
		nums := generateWithCap(10000)
		b.StartTimer()
		bubbleSort(nums)
	}
}

执行该用例,每次排序耗时约 0.05s。

➜  benchmark_test git:(main) ✗ go test sort_test.go -v -bench=BubbleSort$
goos: darwin
goarch: arm64
BenchmarkBubbleSort
BenchmarkBubbleSort-8                 21          55139538 ns/op
PASS
ok      command-line-arguments  1.602s

附 推荐与参考

posted @   aswangc  阅读(108)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示