go test基础用法拾遗
单元测试文件
TestMain
func TestMain(m *testing.M) {
os.Exit(m.Run())
}
一个目录下所有单元测试文件中只能有一个TestMain函数
执行go test时, 先执行TestMain, 执行至m.Run()时再执行具体的单元测试用例, 环境的初始化和资源释放等可以放在TestMain里执行。
需要注意最好使用m.Run()的返回值作为进程退出的返回码 否则即使对存在单元测试用例不通过的情况也是返回错误码0, 如果是shell脚本需要用到这个返回值做一些判断则得不到想要的结果
SubTests
测试某个方法时, 使用子测试的函数来区分不同场景用例,能使单测的代码具有更好的阅读性和维护性。这种场景更推荐table-driven tests的写法
// calc_test.go
func TestMul(t *testing.T) {
cases := []struct {
Name string
A, B, Expected int
}{
{"pos", 2, 3, 6},
{"neg", 2, -3, -6},
{"zero", 2, 0, 0},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
if ans := Mul(c.A, c.B); ans != c.Expected {
t.Fatalf("%d * %d expected %d, but %d got",
c.A, c.B, c.Expected, ans)
}
})
}
}
go test命令
执行指定方法(正则匹配)
go test --run Test_Method1
指定指定方法的指定subtest
用 / 分隔方法名和subtest的name
go test --run Test_Method1/subtest_name
执行多个包的单元测试
执行路径下所有单元测试(...表示递归)
go test ./...
执行指定路径单元测试(空格分隔)
go test ./pkg/... ./pkg1 ./pkg2
输出代码覆盖率以及生产代码覆盖率统计文件
默认统计所有执行到的包的覆盖率
go test --coverprofile=cp.out
指定需要统计的包(多个路径用逗号隔开)
go test coverpkg=./pkg/...,./pkg1,./pkg2
格式化显示代码覆盖率
html
go tool cover --html=cp.out
json
go tool cover --json=cp.out
text
go tool cover --test=cp.out
第三方库
这些第三方库或需根据项目和开发的自身情况进行一定的封装。
table-driven tests代码自动生成
https://github.com/cweill/gotests
不过实际使用时感觉也有不灵活之处, 如果有需要也可以直接使用
- go/build
- go/ast
自己写一个项目代码分析+代码生成的二进制工具
数据库mock
https://github.com/DATA-DOG/go-sqlmock
生成的一个mocker对象用来执行对db的mock设定, 与此同时额外生成一个官方库sql.DB的interface。这个interface的行为受mocker对象控制(query和exec的返回结果)。
只要orm的库操作db的时候, 是通过sql.DB操作, 只要将go-sqlmock替换掉真正连接数据库的sql.DB即可。当然也需要ORM库提供使用sql.DB初始化的方法(如GORM v1,v2都支持)
实际使用时, 应该还需要根据自己的orm以及需求对该包做一些简单的封装。且数据用例应该是可以复用的, 这些数据用例建议以go的model层的结构体或者文件的形式维护, 再做相应的读取封装。 目前笔者的实践是以结构体的形式维护, 方便代码跳转。
redis mock
https://github.com/alicebob/miniredis
相比sqlmock的mock方式, 这个第三方包的mock方式就简单粗暴一点, 直接在内存里起一个基本支持redis所有命令的迷你的redis服务端(会占用一个端口)。 在进行单元测试时, 将客户端的连接指向这个迷你的redis服务器即可。
所有的redis命令也可以通过启动mini redis server时生成的*miniredis.Server来操作, 增删需要的kv
任意interface的mock
https://github.com/golang/mock
由于go不支持动态给struct增加/修改方法, 所以要mock一个interface, 只能新增一个实现了该interface的struct。
go官方提供的这个第三方包提供了mockgen这个根据interface生成mock struct代码的二进制文件, 单元测试时使用mockgen生成的struct即可。
断言
https://github.com/stretchr/testify
该包还提供了其他功能
断言使用的是github.com/stretchr/testify/assert
对*testing.T一些方法的封装, 方便对被测试方法的各种类型结果进行断言并在断言失败时友好地输出错误信息
个人Go单元测试实践总结
- 一般table-driven例子里, 是直接指定的输出结果, 然后对方法返回值做相等校验。 然而实际上如果是web业务常会有一些函数返回的数据较多, 如果期望结果强校验或许有些不便以及不灵活, 可以让用例提供断言函数(入参中有被测方法返回值), 根据用例自定义该对结果断言(可以是全等)
- 为了代码的可单元测试, 业务代码中外部依赖IO时, 除非有go-sqlmock, miniredis这样方便的mock库,否则使用interface方式解耦(如消息队列的客户端, grpc客户端等)
- 为了减少对路径的依赖, 配置文件的数据结构可以单独为单元测试写一个不依赖于配置文件的初始化函数。
- 全局环境, 数据结构, 配置的初始化等抽到一个函数, 放在TestMain里做
- 可以在项目中维护一个统计项目级别单元测试覆盖率, 用例是否通过等的shell脚本,用来方便统计查看项目的单元测试情况以及提供给CI/CD使用
- 单元测试的重点应该是分支的覆盖率而非参数的边界性
- 开发时可以以单元测试驱动开发, 定义好能走到各个分支的单元测试用例。开发接口/函数时先走通单元测试用例再进行真正接口级别依赖外部IO的自测