Tests and Benchmarks in Go

2021/03/01

本次分享我们来了解 Go 中的测试如何编写。

在 Go 中,*_test.go 源码文件,是测试文件。仅在执行 go test 命令时,测试文件中的测试函数会被编译运行。

一般将对独立函数的测试代码与起源码置于同级目录下,例如:

play
├── fib.go
└── fib_test.go

需要整合多个模块的集成测试,设计一个 test 包,例如:

api/test
├── create_normal_user_test.go
├── create_admin_user_test.go
└── update_user_test.go

功能测试

测试一个独立函数的功能,最流行的测试模式是 Table Driven Tests 。

为了确定一个函数的功能实现是否符合预期,我们需要准备好一批的输入和期望输出。

将函数作为黑盒,我们可以将输入和输出类型定义在结构体中,通过循环执行测试用例。

从一个简单的例子开始

实现一个函数,计算第 n 个斐波那契数。

函数实现写在 fib.go 文件中:

package fib
func Fib(n int) int {
        if n < 2 {
                return n
        }
        return Fib(n-1) + Fib(n-2)
}

设计一个测试用的结构体,包含输入和预期输出:

type fibTest struct {
        n        int // input
        expected int // expected result
}

测试函数写在 fib_test.go 中:

func TestFib(t *testing.T) {
        var fibTests = []fibTest {
            {1, 1}, 
            {2, 1}, 
            {3, 2}, 
            {4, 3}, 
            {5, 5}, 
            {6, 8}, 
            {7, 13},
        }
        for _, tt := range fibTests {
                actual := Fib(tt.n)
                if actual != tt.expected {
                        t.Errorf("Fib(%d): expected %d, actual %d", tt.n, tt.expected, actual)
                }
        }
}

运行效果 https://play.golang.org/p/U1krMF0dzE6

运行测试

go test .

如果测试函数过多,通过向 -run 参数传入正则来筛选需要执行的测试。

小结

Table Driven Tests 能够编写出简洁的单元测试,能够以很少的代码得到很大的测试覆盖率。只要设计得当,添加其他测试用例就像在测试表中添加一个新元素一样简单。

这也不是编写 Table Driven Tests 的唯一方式, Go 标准库中还有很多技巧值得学习,可以先从 math 包和 time 包的测试开始学。

基准测试

基准测试也是写在 *_test.go 文件中,需要遵循测试规则。比如我们测试一下计算第 10 个斐波那契数,fib_test.go 中添加以下函数:

func BenchmarkFib10(b *testing.B) {
        // run the Fib function b.N times
        for n := 0; n < b.N; n++ {
                Fib(10)
        }
}

编写基准测试的区别:

运行基准测试

go test -bench .

-bench 需要接收一个正则,你可以利用它筛选需要执行的基准测试。

如果只需要运行基准测试,可以把测试函数屏蔽,传入一个没有匹配结果的正则即可 -run=XXX

许多输入的基准测试

原本 fib 函数是递归实现,它的运行时间也应是随输入值变大而指数增长。

重写这个基准测试:

func benchmarkFib(i int, b *testing.B) {
        for n := 0; n < b.N; n++ {
                Fib(i)
        }
}

func BenchmarkFib1(b *testing.B)  { benchmarkFib(1, b) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(2, b) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(3, b) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) }

私有化 benchmarkFib 函数,可以避免运行测试时用正则匹配到它的函数签名。

参考