unit test

golang unit test

在软件开发的过程中或多或少的引入一些 bug,而排查 bug 的难易程度也有一下分布

self check < unit test < smoke test < qa test < production accident 

以上对产品的影响也是逐级递增的,所以越早发现 bug 对后期的影响也会越小,这里就记录下 golang 中单元测试(Test-driven development)的技巧和规范:

TableDrivenTests

在之前写测试代码的时候经常发现自己写一个新的用例都是用的 copypaste,写多了的时候我们是不是可以停下来想下有什么好的办法去减少重复劳动, 官方有推荐了一种方法 table-driven tests:

  • 初始化一个测试用例列表,每个 entry 中包含了输入和预期结果,还可以包含一些提高阅读性的额外信息,如 name 等,然后遍历这个数组,去执行测试代码,对于代码的执行结果和预期结果。这样每次添加新的用例的时候只需要添加一个新的 entry 即可,不再需要复制粘贴了。

下面是一个例子:

var flagtests = []struct {
	in  string
	out string
}{
	{"%a", "[%a]"},
	{"%-a", "[%-a]"},
	{"%+a", "[%+a]"},
	{"%#a", "[%#a]"},
	{"% a", "[% a]"},
	{"%0a", "[%0a]"},
	{"%1.2a", "[%1.2a]"},
	{"%-1.2a", "[%-1.2a]"},
	{"%+1.2a", "[%+1.2a]"},
	{"%-+1.2a", "[%+-1.2a]"},
	{"%-+1.2abc", "[%+-1.2a]bc"},
	{"%-1.2abc", "[%-1.2a]bc"},
}
func TestFlagParser(t *testing.T) {
	var flagprinter flagPrinter
	for _, tt := range flagtests {
		t.Run(tt.in, func(t *testing.T) {
			s := Sprintf(tt.in, &flagprinter)
			if s != tt.out {
				t.Errorf("got %q, want %q", s, tt.out)
			}
		})
	}
}

但程执行结果和预期不符的时候使用 t.Errorf 输出即可,还有一种选项是 t.Fatalf,后者会在有错误的时候立刻停止测试用例,而前者会跑完所有的用例,推荐第一种。还可以使测试并行执行:

package main

import (
	"testing"
)

func TestTLog(t *testing.T) {
	t.Parallel() // marks TLog as capable of running in parallel with other tests
	tests := []struct {
		name string
	}{
		{"test 1"},
		{"test 2"},
		{"test 3"},
		{"test 4"},
	}
	for _, tt := range tests {
		tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel() // marks each test case as capable of running in parallel with each other 
			t.Log(tt.name)
		})
	}
}

上面需要注意一个点,那就是在遍历测试的需要重新赋值,tt := tt,这点非常重要,在 parallel 执行的时候会启动 goroutines 去执行测试代码,如果这时候还是使用原来的 tt 的话每次遍历执行的值都指向了一个地址,会导致很多测试用例被漏掉,更详细的可以参考上面的链接。

Separate Your Go Tests with Build Tags

在之前 c++ 代码中,可以根据定义的宏来决定某段代码是否执行,之前一直以为 golang 中是没有这个功能的,后来才发现是我孤陋寡闻了,golang 已经提供了 Build Tags 的功能了。

Build Tags

build tags 就是在 .go 源文件的顶部提供单行注释来告诉编译器在执行 go build 的时候是否要处理这个文件。下面这个文件就只是在有 GOOS=linux 环境变量的时候才会去编译。

// +build linux

package mypackage

...

这个 build tag 需要尽可能的放到文件的顶部,并且需要在下方有个空行。这个 tags 同样可以用 ! 来表示否定。 // +build !linux 将会表示在非 linux 环境下被编译。tag 同样支持多个条件判断,如果是在一行的表示 的逻辑,如果是分成两行则表示 的逻辑。

// +build linux darwin
// +build amd64

package mypackage

...

上述就表示了会在 linux/amd64darwin/amd64 的环境中被编译。

Separating Tests

上面简单的描述了下 Build Tags,那么我们就可以利用这个特性来跑不同类型的测试用例了。

我们在跑测试用例的时候可能会有 integrationunit 的例子,我们下面列出两个例子怎么去区分他们。

myfile_test.go:

package mypackage

import "testing"

...

myfile_integration_test.go:

// +build integration

package mypackage

import "testing"

...

当我们去执行 go test 的时候,就只有 myfile_test.go 文件的用例被执行了,如果想让 myfile_integration_test.go 中的测试用例也被执行,就需要提供一个 tags 参数,即go test --tags=integration

这里还有点小问题,就是在执行 go test --tags=integration 的时候,myfile_test.go 中的用例也会被执行,如果你不想他被执行的话,就是按照如下更改:

// +build !integration

package mypackage

import "testing"

...

Put your tests in a different package

正常来说一个 go 目录下只允许有一个包名,如果存在两个报名的话会有报错,但是却又特例:那就是 _tes.go 文件。

当你想测试外部接口的时候,同时排除内部接口的干扰,这个就非常合适了,当测试文件定义了一个新的报名,调用这个文件就相当于是从外部调用。例子如下:

file.go:

package file

func Open() {
    ...
}

file_test.go:

package file_test

import (
    "xxx/file"
)

func TestOpen() {
    file.Open()
}

Reference