Go的测试

警告
本文最后更新于 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 限制。

子测试可以将单元测试拆分为多个小测试用例,提高它们的可读性、可维护性和精准度。可用于 TB 的测试。**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。**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

相关内容