go test - 单元测试

ddatsh

dev #go 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 函数
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }

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

常用命令/参数


# 执行当前目录下的全部测试用例,不递归子目录中的测试用例
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

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

方法:

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
Fail : 测试失败,测试继续,之后的代码依然会执行
FailNow : 测试失败,测试中断
go help test
go help testflag
go mod init example

单元测试

calc

example/
   |--calc.go
   |--calc_test.go
// calc.go
package main

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

func Mul(a int, b int) int {
	return a * b
}
// 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)
	}
}
example/
     |--fib.go
     |--fib_test.go
// fib.go
package main

func Fib(n int) int {

	if n == 0 || n == 1 {
		return n
	}
	return Fib(n-2) + Fib(n-1)
}
// 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

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 方法的测试并行进行测试

// 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]
}
// 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

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

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

-coverpkg

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

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

https://github.com/asspirin12/go_cover

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

分析结果

perf/cmd 提供了软件包

benchstat 可用于分析结果

benchsave 可用于保存结果

Example

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

套件

ref