go test - 单元测试

go test 常见做单测试、基准测试和 http 测试

go test 还有很多 flag 可以帮助我们做更多的分析,比如测试覆盖率,cpu 分析,内存分析,也有很多第三方的库支持 test,cpu 和内存分析输出结果要配合 pprof 和 go-torch 来进行可视化显示

命名

文件名 _test.go 结尾,前缀后的第一个字母必须大写

类型 函数名前缀 作用
测试函数 Test 测试程序的一些逻辑行为是否正确
基准函数 Benchmark 测试函数的性能
模糊测试 Fuzz 测试程序的健壮性
示例函数 Example 为文档提供示例文档
main函数 TestMain(m *testing.M) 测试 main 函数
1
2
3
4
5
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

func BenchmarkAdd(b *testing.B) { ... }

常用命令/参数

  • -v:显示详细的测试输出,包括每个测试用例的名称和结果
  • -run:指定要运行的测试函数的正则表达式
  • -cover:同时进行代码覆盖率分析,显示代码被执行的情况
  • -coverprofile:将代码覆盖率分析的结果输出到指定文件中
  • -count:指定测试的运行次数,默认为 1 次
  • -timeout:设置测试的运行超时时间
  • -bench:运行与性能测试有关的基准测试
  • -benchmem:在运行基准测试时显示内存分配的统计信息

  • 运行当前 package 内的用例:go test examplego test .
  • 运行子 package 内的用例: go test example/<package name>go test ./<package name>
  • 递归测试当前目录下的所有的 package:go test ./...go test example/...
  • 执行特定的测试用例 go test -v . -test.run 'Fib'
  • 默认不输出 log,-v 输出
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 执行当前目录下的全部测试用例,不递归子目录中的测试用例
go test .
# 执行当前目录下的全部测试用例,递归子目录中的测试用例
go test ./...
# 执行当前目录下的全部测试用例并显示测试过程中的日志内容,不递归子目录中的测试用例
go test -v .
# 执行当前目录下的全部测试用例并显示测试过程中的日志内容,递归子目录中的测试用例
go test -v ./...
# 执行指定的测试用例
go test -v . -test.run '^TestValid$'
# 测试所有包含 user(不区分大小写) 的测试方法
go test -v -run="(?i)user"
# 测试多个方法,名称用 "|" 分开
go test -v -run="TestGetOrderList|TestNewUserInfo"

*testing.T

管理测试状态并支持格式化测试日志

方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func (c *T) Cleanup(func())
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Helper()
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
func (c *T) TempDir() string
1
2
Fail : 测试失败,测试继续,之后的代码依然会执行
FailNow : 测试失败,测试中断
1
2
go help test
go help testflag
1
go mod init example

单元测试

calc

1
2
3
example/
   |--calc.go
   |--calc_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// calc.go
package main

func Add(a int, b int) int {
	return a + b
}

func Mul(a int, b int) int {
	return a * b
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// calc_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
	if ans := Add(1, 2); ans != 3 {
		t.Errorf("1 + 2 expected be 3, but %d got", ans)
	}

	if ans := Add(-10, -20); ans != -30 {
		t.Errorf("-10 + -20 expected be -30, but %d got", ans)
	}
}
1
2
3
example/
     |--fib.go
     |--fib_test.go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// fib.go
package main

func Fib(n int) int {

	if n == 0 || n == 1 {
		return n
	}
	return Fib(n-2) + Fib(n-1)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// fib_test.go
package main

import "testing"

func TestFib(t *testing.T) {
	var (
		in       = 7
		expected = 13
	)
	actual := Fib(in)
	if actual != expected {
		t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
	}
}

Table-Driven Test

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func TestFib(t *testing.T) {
    var fibTests = []struct {
        in       int // input
        expected int // expected result
    }{
        {1, 1},
        {2, 1},
        {3, 2},
        {4, 3},
        {5, 5},
        {6, 8},
        {7, 13},
    }

    for _, tt := range fibTests {
        actual := Fib(tt.in)
        if actual != tt.expected {
            t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
        }
    }
}

t.Errorf,即使其中某个 case 失败,也不会终止测试执行

Parallel

当前测试只会与其他带有 Parallel 方法的测试并行进行测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// parallel.go
package main

import "sync"

var (
	data   = make(map[string]string)
	locker sync.RWMutex
)

func WriteToMap(k, v string) {
	locker.Lock()
	defer locker.Unlock()
	data[k] = v
}

func ReadFromMap(k string) string {
	locker.RLock()
	defer locker.RUnlock()
	return data[k]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// parallel_test.go
package main

import "testing"

var pairs = []struct {
	k string
	v string
}{
	{"go", " go语言"},
	{"stdlib", "Go 语言标准库"},
}

// 注意 TestWriteToMap 需要在 TestReadFromMap 之前
func TestWriteToMap(t *testing.T) {
	t.Parallel()
	for _, tt := range pairs {
		WriteToMap(tt.k, tt.v)
	}
}

func TestReadFromMap(t *testing.T) {
	t.Parallel()
	for _, tt := range pairs {
		actual := ReadFromMap(tt.k)
		if actual != tt.v {
			t.Errorf("the value of key(%s) is %s, expected: %s", tt.k, actual, tt.v)
		}
	}
}

步骤:

  1. 注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,同时注释掉测试代码中的 t.Parallel,执行测试,测试通过,即使加上 -race,测试依然通过
  2. 只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,执行测试,测试失败(如果未失败,加上 -race 一定会失败)

如果代码能够进行并行测试,在写测试时,尽量加上 Parallel,这样可以测试出一些可能的问题

TempDir

自动为测试创建临时目录并在测试完成时删除该文件夹的方法,无需编写额外清理逻辑

1
2
3
4
func TestFooerTempDir(t *testing.T) {
    tmpDir := t.TempDir()
  // your tests
}

Coverage

go test -cover

go test -v . -coverprofile=coverage.out -covermode=atomic

go tool cover -html=coverage.out -o cover.html

  • -coverprofile 指定覆盖测试结果文件

  • -covermode 指定覆盖测试的方式,set, count, atomic 三个值,默认值 set,-race 时默认 atomic,绝大多数情况下可以统一使用 atomic

    • set:覆盖率基于语句
    • count:计数是一条语句运行的次数。 它可以显示代码的哪些部分仅被轻微覆盖
    • atomic:与计数类似,但用于并行测试

    GoLand 默认使用 atomic 模式

-coverpkg

go test 覆盖率计算中只考虑带有测试文件的软件包。 -coverpkg 将所有软件包添加到覆盖率计算中

go test ./... -coverpkg=./...

https://github.com/asspirin12/go_cover

详细展示了您需要 -coverpkg 的原因

分析结果

perf/cmd 提供了软件包

benchstat 可用于分析结果

benchsave 可用于保存结果

Example

1
2
3
4
func ExampleFib() {
	fmt.Println(Fib(1))
	// Output: 1
}

套件

  • testify 做 assert 判断
  • testify 构建 单元测试集合,可以写 setup/teardown
  • gomonkey 做 变量,函数,普通的成员方法(公有,私有)的 mock
  • mockery 做接口层面 mock
  • miniredis 做 redis 的 mock

ref

series: