使用单测提高go代码质量 | 青训营笔记

91 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记,青训营day2主要介绍了go的工程化实践,本篇blog将集中总结go单元测试相关内容,下面先看一下go单测的基本写法。

单元测试基本用法

go test对文件名字有一定要求,单测文件必须以_test.go为结尾,例如被测文件名字为math.go,对应单测文件名字就是math_test.go。

下面创建一个matht包,写一下对应的的测试case

package matht

import (
	"math"
	"math/rand"
)

func Abs(x float64) float64 {
	return math.Abs(x)
}

func Max(x, y float64) float64 {
	return math.Max(x, y)
}

func Min(x, y float64) float64 {
	return math.Min(x, y)
}

func RandInt() int {
	return rand.Int()
}

在同一个package下创建matht_test.go

package matht

import "testing"

func TestAbs(t *testing.T){
	got := Abs(-1)

	if got != 1{
		t.Errorf("Abs(-1) = %f; want 1", got)
	}
}

test函数有几个要注意的点

  • 函数名必须以Test开头,例如测试Abs则测试函数名字为TestAbs
  • 参数必须为*testing.T类型

然后执行go test即可运行单测(go test还支持-v,-count,-cover等flag)

go test ./...

go test ./... -v 显示细节
go test ./... -count=2 单测运行两次
go test ./... -cover 测试覆盖率

为了提高测试可靠性,可以在一个case中运行多个测试用例,下面一个TestAbs为例

func TestAbs(t *testing.T){
	tests := []struct{
		value float64
		want float64
	}{
		{-1, 1},
		{1, 1},
		{0, 0},
	}

	for _, tt := range tests{
		if got := Abs(tt.value); got != tt.want{
			t.Errorf("Abs() = %f, want %v", got, tt.want)
		}
	}
}

到这里可以发现go提供的基础测试包相对于jest这种完善的测试库是比较粗糙的,只有最基本的功能。

使用更完善的断言库

go社区提供了assert等功能更丰富的断言库,毕竟原生的if error代码比较难看。下面使用assert优化一下前面的测试用例

for _, tt := range tests{
	got := Abs(tt.value)
	assert.Equal(t, got, tt.want)
}

除了Equal,assert还提供了NotEqual,Less,True,Nil等常用的断言功能,基本可以满足日常测试需要。

mock测试

简单解释一下mock,后端的mock和前端的mock差不多,有时候我们需要测试一段代码,但是该代码可能涉及连接数据库/redis等功能,这一部分功能并不包括在单测内(单测并不关心外部服务的状态,只专注于函数功能),所以需要对连接数据库的部分进行替换,这就是mock。

下面这个例子用来mock一个读取文件的函数,毕竟在测试过程中并不能真的去读一个文件(一方面是读文件太慢,另一方面是文件并不包含在单测范围内),monkey库直接替换掉了被mock函数的地址。

func TestProcessFirstLineWithMock(t *testing.T){
	monkey.Patch(ReadFirstLine, func() string{
		return "line110"
	})
	defer monkey.Unpatch(ReadFirstLine)
	line := ProcessFirstLine()
	assert.Equal(t, "line000", line)
}

自动生成测试用例

看完前面的代码可以发现单测的文件格式比较固定,都是给对应的函数生成测试函数,那么固定的部分就可以用工具生成,开发者只需要关注测试的核心部分就可以了(对于前端来说测试的情况比较复杂,自动生成的意义可能不是特别大)。

下面使用gotests自动生成测试用例

go get -u github.com/cweill/gotests/...

//将gotests配置到path
gotests -all -w .

然后只需要补全测试代码核心部分即可

benchmark性能测试

和单测要求一样,benchmark测试要求测试函数必须以Benchmark开头。

下面测试一下rand函数

func BenchmarkRandInt(b *testing.B) {
	for i := 0; i < b.N; i++ {
		RandInt()
	}
}

运行go test

go test -bench=".*" ./...

结果如下

goos: windows
goarch: amd64
pkg: study/matht
BenchmarkRandInt-8      70540101                16.5 ns/op
PASS
ok      study/matht     1.613s

解释一下输出的几个字段

  • BenchmarkRandInt-8说明有8个cpu参与了测试
  • 70540101 是循环次数
  • 16.5 ns/op 每次循环消耗16.8纳秒(移动端的u雀实不太行)

summary

总结一下go单测的重点内容

  • go的单测和其他语言差不多,对于函数及文件名有特定的规范
  • go虽然提供了默认test库,但是功能比较简陋,配合asset等库实现更强大的功能。
  • mock是单测中的重要部分
  • 对于性能敏感的函数需要进行benchmark测试