这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
今天和大家分享如何对go程序进行测试
单元测试
单元测试概念
基本概念:
单元测试是面对开发过程进行的测试,测试对象是对开发过程中的相应函数,模块进行测试。
单元测试通过输入相应参数进入测试单元,对输出值和期望值进行校对,来完成对函数和模块的测试。
单元测试是测试成本最低但同时需要更高覆盖率的测试。
测试规则:
-
所有测试文件以
_test.go结尾 -
函数命名和参数模板为:
func TestXxx(*testing.T)。这里的testing是go语言的内置包。每一个测试函数都可以独立运行。 -
初始化逻辑放到
TestMain函数中:import "testing" func TestMain(m *testing.M) { // 测试前:数据装载,配置初始化等前置工作 code := m.Run() ... // 测试后:释放资源等收尾工作 os.Exit(code) }
测试运行:
运行单元测试,我们可以使用go test [flags] [packages],当然,更常用的是使用IDE自带的运行测试,逐个测试函数运行等按钮。
测试辅助:
我们可以使用assert包来辅助验证,
例子:
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestEqual(t *testing.T) {
output := 1
expectOutput := 1
assert.Equal(t, expectOutput, output)
}
其他参考资料:
测试覆盖率:
代码覆盖率是对整体程序可靠程度的重要评估标准。
计算代码测试的覆盖率可以使用go test xx_test.go xx.go --cover命令,在进行测试的同时就可以得到测试程序对xx.go的测试覆盖率是多少:
// xx.go
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
// xx_test.go
func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70) // 引入待测试函数
assert.Equal(t, true, isPass)
}
得到以下覆盖率数据:
ok command-line-arguments 0.665s coverage: 66.7% of statements
说明其代码覆盖率达到了66.7%。
虽然我们在测试函数中执行了JudgePassLine函数,但是由于我们只验证了70得到true的情况,所以并没有对函数测试完全。我们修改测试函数(或新增测试函数):
func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70) // 引入待测试函数
assert.Equal(t, true, isPass)
notPass := JudgePassLine(30)
assert.Equal(t, false, notPass)
}
重新执行命令,得到以下数据:
ok command-line-arguments 0.687s coverage: 100.0% of statements
表明我们已经对xx.go中的函数做到了完全覆盖。
在实际项目中,我们对需要测试的单元,通常达到50%-60%即可;需要高度测试的单元,我们可以尽量做到80%以上。
mock测试
在日常的项目开发中,一般都会存在很多依赖,例如gorm,gin,os等,我们在使用这些依赖进行项目开发时,对每个模块都进行单元测试会很麻烦,此时我们可以采取mock测试。
我们使用gomonkey测试包进行mock测试,这是一个常用的mock测试包。示例如下:
import (
"bou.ke/monkey"
"github.com/stretchr/testify/assert"
"testing"
)
func TestFnxxx(t *testing.T) {
// 对Fnxxx进行打桩
monkey.Patch(Fnxxx, func() bool {
return true
})
defer monkey.Unpatch(Fnxxx)
...
}
上面示例中提到的打桩,就是将某个函数A替换成打桩函数P.
打桩函数的意义在于:若A函数的使用复杂,返回不规律且可能受各种环境影响不稳定,但是B函数(待测试函数)需要使用到函数A,那么我们就可以使用一个打桩函数P来替换函数A。打桩函数通过可控的返回来测试B。
实例:
// xx.go
// 复杂函数
func FetchSomeApi() string {
// ...一系列fetch操作
}
// 待测试函数
func ProcessFetchSomeApi() string {
str := FetchSomeApi()
return strings.ReplaceAll(str, "a", "b")
}
// xx_test.go
func TestProcessFetchSomeApi(t *testing.T) {
monkey.Patch(FetchSomeApi, func() string {
return "abc"
})
defer monkey.Unpatch(FetchSomeApi)
strProcessed := ProcessFetchSomeApi()
assert.Equal(t, "bbc", strProcessed)
}
注意这里进行测试运行的时候,由于golong使用了内部优化,导致打桩会失效。我们可以使用命令行进行测试:
go test xx_test.go xx.go -gcflags=all=-l
基准测试
基准测试是测试一段程序来查看cpu的损耗,我们通常对程序进行基准测试来分析程序性能,来找到瓶颈和优化点。
基准测试和单元测试规则相似,其命名规则为BenchmarkXxx。
例如,我们对上面单元测试的JudgePassLine函数进行基准测试:
func BenchmarkJudgePassLine(b *testing.B) {
// 重置操作,在重置操作之前,我们可以执行一些其他准备函数,不会记录在性能中
b.ResetTimer()
// 注意for循环使用b.N来模拟大量的循环触发
for i := 0; i < b.N; i++ {
JudgePassLine(70)
}
}
得到测试结果:
BenchmarkJudgePassLine-16 1000000000 0.2906 ns/op
表明执行了1000000000次花费了0.2906ns。
上述的循环操作是串行的,我们可以使用并行来重新测试:
func BenchmarkJudgePassLine(b *testing.B) {
// 重置操作,在重置操作之前,我们可以执行一些其他准备函数,不会记录在性能中
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
JudgePassLine(70)
}
})
}
得到测试结果:
BenchmarkJudgePassLine-16 100000000 18.00 ns/op
可以看出,当程序比较简单的时候,其实使用串行比并行更加有效率。