在golang语言中,通过go test和一些特殊的规则能快速实现单元测试。再正式介绍之前,先说一个坑。

坑:相同package中不同文件里的函数调用

在如下所示的目录结构中,我希望从main中调用add.go里的Add函数实现加法功能。

- test
 -- main.go
 -- add.go

然而通过go run main.go后,一直提示add函数未定义,而Add的确是定义过了。

# command-line-arguments
./main.go:9:14: undefined: Add

排查后才发现这种目录结构和场景时,不能使用go run main.go进行编译执行,而应该使用:

go run main.go add.go //需要加上全部文件

所以建议大家使用类似如下所示的目录结构进行项目开发,main.go文件单独放在主目录下,其他文件放在utils等新的目录中,使用import关键词导入包。

- test
 -- main.go
 - utils
   -- add.go

下面我们正式讲解golang项目的测试代码。优秀的开发团队会针对重要的函数进行单元测试,同时良好的单元测试能够保证代码稳定性,为今后添加新功能提供便利。

逻辑测试

golang语言提供go test命令进行功能测试,即测试函数的正确性。下面用一个简单的示例带领大家逐步熟悉。假设我们写了一个utils的工具库,里面包含一个int型加法函数,需要我们进行测试。加法函数如下:

package utils

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

那如何对这个函数进行测试呢?需要编写测试程序,同时该测试程序需要按照一定的规则,以便于利用go test进行自动化编译运行。

1、在add.go的同级目录下心间add_test.go测试文件;测试文件的名称必须是源文件加上 “_test.go” 的文件,go test能够自动识别。即

- test
 -- main.go
 - utils
   -- add.go
   -- add_test.go

2、在add_test.go中编写TestAdd测试函数;测试函数名必须是“Test”前缀加上源函数名

package utils

import "testing"
import "reflect"

func TestAddInt(t *testing.T){ //参数必须为 t *testing.T
    got := AddInt(2, 4)
    want:= 5 //6 != 5 测试结果为失败
    if !reflect.DeepEqual(want, got) {//深度比较,简单的数据可以直接用==比较
        t.Errorf("excepted:%v, got:%v", want, got)//t.Errorf用于判断测试是否
                      //通过,不可省略,否则程序如果顺利执行完则判定为测试通过。
    }
}

在utils目录下执行

go test -v //完成自动化测试

如果一个文件里有很多个函数都需要测试,那可以按照上述1和2规则完成测试函数后进行测试。同学们可是尝试添加一个AddFloat函数并完成测试函数。

往往一组测试用例是不够的,所以针对每个函数需要多组测试用例进行测试。golang提供测试组以批量完成测试,同时也支持测试单个用例。简单的说就是对将测试用例分成组,同时起名。遍历完成批量测试,制定名称完成单个测试。

package utils

import "testing"
import "reflect"

func TestAddInt(t *testing.T){
    type testCase struct {
        addA int
        addB int 
        want int //期望的计算结果
    } 
    testGroup := map[string]testCase{
        "case1": testCase{1,2,3}, 
        "case2": testCase{2,3,5}, 
        "case3": testCase{3,4,7}, 
    }

    for key, v := range testGroup { //遍历
        t.Run(key, func (t *testing.T){
            got := AddInt(v.addA, v.addB)
            want:= v.want
            if !reflect.DeepEqual(want, got) { //比较
                t.Errorf("excepted:%v, got:%v", want, got)
            }
        })
    }
}

上述代码是批量测试,而如果需要单独测试某个示例,则只需要通过map的name进行控制即可。通过执行go test -v,得到如下结果。

➜  utils go test -v
=== RUN   TestAddInt
=== RUN   TestAddInt/case3
=== RUN   TestAddInt/case1
=== RUN   TestAddInt/case2
--- PASS: TestAddInt (0.00s)
    --- PASS: TestAddInt/case3 (0.00s)
    --- PASS: TestAddInt/case1 (0.00s)
    --- PASS: TestAddInt/case2 (0.00s)
PASS
ok      day1/common/utils       0.006s

如果测试全部通过,则说明函数功能没有问题;反之需要检查函数逻辑。

性能测试

同逻辑测试类似,我们只需要在测试文件中加上对应的基准测试函数即可,基准函数名称以Benchmark开头,则在本例中为BenchmarkAddInt,具体函数如下:

func BenchmarkAddInt(b *testing.B) {
	for i := 0; i < b.N; i++ {
		AddInt(12, 13)
	}
}

其中N是testing.B结构体中的一个变量,可以认为是测试函数AddInt在固定时间内可执行的次数。例如如下命令完成测试:

go test -bench=AddInt -benchmem //即对AddInt函数进行基准测试

输出结果为:

➜  utils go test -bench=AddInt -benchmem
goos: darwin //操作系统
goarch: amd64 //架构
pkg: day1/common/utils
BenchmarkAddInt-12  1000000000    0.261 ns/op  0 B/op    0 allocs/op
PASS
ok      day1/common/utils       0.301s

如上所示,在12核的机器上,执行1000000000次AddInt,每次操作耗费时间0.261纳秒,空间0B,每次不分配空间,因为我们使用的是内置的int型,不需要分配内存。同时本次测试PASS。