警告
本文最后更新于 2023-03-14,文中内容可能已过时。
详细的参数信息可通过 go help testflag
查看。下面是一些常用的测试参数。
-v
: 输出详细的测试信息,包括每个测试的运行结果;
-run regexp
: 根据正则表达式来指定要运行的测试函数;
-cover
: 开启代码覆盖率统计功能,查看测试覆盖率的报告;
-bench regexp
: 运行基准测试,并根据正则表达式来筛选要运行的基准测试函数;
-cpu n
: 设置并发执行的处理器核心数量;
-memprofile file
: 在运行测试时生成一个内存 profile 文件,并将其保存到指定的文件中,然后可以使用 pprof 工具来分析该文件;
-count n
: 设置需要运行的测试和基准测试的次数。
-benchmem
: 测试结果包含内存的分配率。
下面示例包括两个测试函数,分别测试了通过 Add
函数和 Substruct
函数实现基本的加减功能。每个测试使用 T
类型参数作为输入,并在需要时引发错误。
1
2
3
4
5
6
7
8
|
// simple.go
func Add(a int, b int) int {
return a + b
}
func Subtract(a int, b int) int {
return a - b
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// simple_test.go
func TestAddition(t *testing.T) {
total := Add(3, 7)
if total != 10 {
t.Errorf("Add function returned an incorrect value. Got %d, expected %d", total, 10)
}
}
func TestSubtraction(t *testing.T) {
difference := Subtract(9, 4)
if difference != 5 {
t.Errorf("Subtract function returned an incorrect value. Got %d, expected %d", difference, 5)
}
}
|
1
2
|
# 运行命令
$ go test -v .
|
覆盖测试是一种软件测试技术,用于衡量测试用例能够覆盖代码中多少的语句、分支、函数、条件等。它可以提供有关程序源代码的结构和质量的信息,以及帮助评估测试用例的质量。
覆盖测试通常结合使用代码覆盖工具来分析单元测试结果,并生成报告。这些报告可以显示哪些代码行得到了覆盖,并且哪些行从未被执行过。通过改进没有足够覆盖率的代码测试,可以增加代码的可靠性,减少缺陷率并提高开发效率。
但是注意,拥有 100%
的测试覆盖率并不意味着应用程序没有 Bug
。
例如下面创建两个文件,cover.go
是代码,cover_test.go
为测试代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// cover.go
package size
func Size(a int) string {
switch {
case a < 0:
return "negative"
case a == 0:
return "zero"
case a < 10:
return "small"
case a < 100:
return "big"
case a < 1000:
return "huge"
}
return "enormous"
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// cover_test.go
package size
import "testing"
type Test struct {
in int
out string
}
var tests = []Test{
{-1, "negative"},
{5, "small"},
}
func TestSize(t *testing.T) {
for i, test := range tests {
size := Size(test.in)
if size != test.out {
t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out)
}
}
}
|
执行下面代码就能看到覆盖率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
# 运行覆盖测试
$ go test -cover
PASS
coverage: 42.9% of statements
# 将覆盖测试的详细信息输出到文件。
$ go test -coverprofile=coverage.out
# 和 go test -cover 输出一样
$ go tool cover -func=coverage.out
# 在游览器中查看哪些代码没有被覆盖
$ go tool cover -html=coverage.out
# -coverpkg控制覆盖测试的范围
$ go test -coverpkg=./... -coverprofile=coverage.out ./...
|
go test 命令接受一个 -covermode
标志来将覆盖测试的模式:
- set:每个语句都运行了吗?默认设置。
- count:每条语句运行了多少次?
- atomic:类似于计数,但在并行程序中精确计数。
1
2
|
# 通过 count 模式生成 fmt 的覆盖测试报告
$ go test -covermode=count -coverprofile=count.out fmt
|
基准测试(benchmark testing)可以用于衡量代码的性能。通过执行一组重复调用某个函数或方法的基准测试,可以测量该函数或方法在处理真实用例时花费的时间,从而可以找出任何瓶颈或优化机会,并确定输入规模等因素如何影响代码的性能。
基准分析工具子存储库包含用于分析Go基准测试结果的工具和包,例如测试包基准测试的输出。
1
2
3
4
5
6
7
8
9
10
11
12
|
// fibonacci.go
func Fibonacci(n int) []int {
sequence := make([]int, n)
if n < 2 {
return sequence
}
sequence[0], sequence[1] = 1, 1
for i := 2; i < n; i++ {
sequence[i] = sequence[i-1] + sequence[i-2]
}
return sequence
}
|
1
2
3
4
5
6
|
// fibonaccic_test.go
func BenchmarkFibonacci(b *testing.B) {
for n := 0; n < b.N; n++ {
Fibonacci(10)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
# 运行命令。35963932次是循环运行,每个循环的速度为32.36 ns
$ go test -v -bench .
BenchmarkFibonacci-16 35963932 32.36 ns/op
# -cpu选项可以指定使用CPU的个数,默认是 NumCPU 的个数
$ go test -cpu 2,4,6 -bench .
# -benchtime设置基准测试的时间
$ go test -v -bench . -benchtime=10s
# -count设置基准测试的次数, tee 是将结果输出到stats.txt并在stdout打印
$ go test -bench=. -count=10 | tee stats.txt
|
如果在测试前需要开销比较大的设置(影响测试结果),可使用 b.ResetTimer()
。
1
2
3
4
5
6
7
|
func BenchmarkBigLen(b *testing.B) {
big := NewBig()
b.ResetTimer()
for i := 0; i < b.N; i++ {
big.Len()
}
}
|
Go Fuzzing是一种自动化测试工具,可以帮助开发人员发现和修复代码中潜在的错误和漏洞。它通过生成随机输入和记录程序行为来实现这一点,然后将结果提供给开发人员进行分析。这个工具适用于处理不规律或复杂输入数据的场景,如从网络传输接收到的数据,解压缩数据等。在这些场景下,手动编写测试用例可能无法覆盖程序的所有路径,并且Go Fuzzing可以帮助开发人员发现之前未考虑到的边界情况和漏洞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// table_test.go
func TestAddition(t *testing.T) {
testCases := []struct { // 定义数据结构来存储测试用例
a, b int // 输入的两个整数
expected int // 预期输出结果
}{
{1, 2, 3},
{-1, 1, 0},
{-2, -3, -5},
{0, 0, 0},
}
for _, tc := range testCases { // 按顺序遍历每个测试用例
result := tc.a + tc.b // 执行被测函数
if result != tc.expected { // 如果输出结果与预期不符,则记录错误信息
t.Errorf("Addition(%d, %d) = %d; want %d", tc.a, tc.b, result, tc.expected)
}
}
}
|
在上面的示例中,我们定义了一个包含四个测试用例的切片 testCases
,每个测试用例都包含两个输入参数和一个预期的输出结果。
随后,我们循环遍历每个测试用例,并调用需要测试的函数(这里是加法)。最后,我们检查实际输出值是否等于预期输出值,如果出错,则记录错误信息。
对于此例子中的错误记录,t.Errorf()
函数允许我们使用类似 C 中的格式化字符串功能自定义错误消息,以便识别更改后的错误输出和预期结果之间的区别。
因为您可以轻松地添加更多测试用例,而无需重复编写大量单独的测试功能。这样也方便检查线上服务的 bug 限制。
子测试可以将单元测试拆分为多个小测试用例,提高它们的可读性、可维护性和精准度。可用于 T
和 B
的测试。**setup code **是单个测试用例。
1
2
3
4
5
6
7
|
func TestFoo(t *testing.T) {
// <setup code>
t.Run("A=1", func(t *testing.T) { ... })
t.Run("A=2", func(t *testing.T) { ... })
t.Run("B=1", func(t *testing.T) { ... })
// <tear-down code>
}
|
1
2
3
4
|
go test -run '' # 运行所有测试
go test -run Foo # 运行顶层匹配 "Foo", 例如 "TestFooBar"
go test -run Foo/A= # 运行顶层匹配 "Foo", 子测试为 "A="
go test -run /A=1 # 运行顶层匹配 "Foo", 子测试为 "A=1".
|
TestMain
如果这个包的测试需要提前连接数据库,可以使用 TestMain
。**setup code ** 是整个包测试前。
1
2
3
4
5
6
7
8
|
func TestMain(m *testing.M) {
log.Println("Do stuff START the tests!")
// <setup code>
exitVal := m.Run()
// <tear-down code>
log.Println("Do stuff AFTER the tests!")
os.Exit(exitVal)
}
|
注意 从 Go 1.17 开始,语法 // +build foo
被 //go:build foo
取代。暂时(Go 1.18),gofmt 同步这两种形式来帮助迁移
add_test.go
测试文件,打了个 foo 标签。
1
2
3
4
5
6
7
|
package example
import "testing"
func TestFuncAdd(t *testing.T) {
t.Log("testFuncAdd")
}
|
sub_test.go
测试文件,打了个 integration 标签。
1
2
3
4
5
6
7
8
9
|
//go:build integration
package example
import "testing"
func TestFuncSub(t *testing.T) {
t.Log("testFuncSub")
}
|
1
2
3
4
5
|
# 只运行没有标签的测试程序
$ go test -v .
# 只运行没有标签 和 integration标签的测试程序
$ go test --tags=integration -v .
|
如果在 integration
加个 !
,代表在运行 go test --tags=integration -v .
的时候排除这个测试,执行 go test -v .
会被运行。
1
2
3
4
5
6
7
8
9
|
//go:build !integration
package example
import "testing"
func TestFuncSub(t *testing.T) {
t.Log("testFuncSub")
}
|
1
2
3
4
5
6
|
func TestFuncAdd(t *testing.T) {
if os.Getenv("INTEGRATION") != "true" {
t.Skip("skipping integration test")
}
t.Log("testFuncAdd")
}
|
1
2
|
# 当 INTEGRATION=“true” 时,执行测试
$ export INTEGRATION="true"; go test -v .
|
使用标签是适用于整个文件,但使用 short mode 适用于单独的测试。
1
2
3
4
5
6
|
func TestFuncAdd(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
t.Log("testFuncAdd")
}
|
1
2
|
# 跳过有 short mode 的测试
$ go test -short -v .
|
当我们使用 t.Parallel
标记测试时,该测试将与所有其他被标记为 t.Parallel
的测试一起并行执行。然而,在执行方面,Go 首先一个接一个地运行所有非并行测试(即没有标记t.Parallel
),并且按照它们在表中的声明顺序来运行申明的测试函数。当非并行测试完成后,Go 再开始执行并行测试。也可以在子测试中使用。
1
2
3
4
|
func TestFuncAdd(t *testing.T) {
t.Parallel()
t.Log("testFuncAdd")
}
|
默认情况下,可以同时运行的测试的最大数量等于GOMAXPROCS
的值。可以通过 -parallel
来增加并行的数量。
1
|
$ go test -parallel 16 .
|
可以通过添加 -shuffle
来随机测试和基准测试。
1
2
3
4
5
6
|
# 进行随机测试
$ go test -shuffle=on -v .
-test.shuffle 1678809899125701565
# 如果随机测试出了bug,可以通过传入随机种子来复现
$ go test -shuffle=1678809899125701565 -v .
|
- 在测试中不要使用
time.Sleep()
,尽量使用无缓冲 chan
。
- 依赖时间的测试可以用
time.Parse()
解析成固定时间。
- 在测试中开启 Race Detector。
The cover story
Using Subtests and Sub-benchmarks
Go Fuzzing
Code coverage for Go integration tests
Vulnerability Management for Go
https://pkg.go.dev/testing
Don’t use build tags for integration tests