这是我参与「第五届青训营 」笔记创作活动的第 5 天
前言
在前一篇文章中,我们介绍了 Go 依赖管理相关的内容,在实际开发中,另一个重要的概念是测试,本文主要介绍 Go 测试相关的内容,包括单测规范、测试 Mock 以及基准测试。
重点内容
- 单元测试
- Mock 测试
- 基准测试
知识点介绍
测试关系着系统的质量,质量则决定线上系统的稳定性。只要做好完备的测试,就可以避免事故的发生。
测试一般分为:
- 回归测试:QA 同学手动通过终端回归一些固定的主流程场景
- 集成测试:对系统功能维度做测试验证
- 单元测试:开发者对单独的函数、模块做功能验证
单元测试
规则:
- 所有测试文件以 test.go 结尾
- func TestXxx( *testing.T)
- 初始化逻辑放到 TestMain 中
举一个例子:
func HelloTom() string {
return "Tom"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
运行结果
ok github.com/Moonlight-Zhao/go-project-example/test (cached)
如果 HelloTom 函数改成返回 “Jerry”,则结果为
--- FAIL: TestHelloTom (0.00s)
e:\Every program\test_go\go-project-example-0\test\print_test.go:11:
Error Trace: print_test.go:11
Error: Not equal:
expected: "Tom"
actual : "Jerry"
Diff:
--- Expected
+++ Actual
@@ -1 +1 @@
-Tom
+Jerry
Test: TestHelloTom
FAIL
覆盖率:衡量代码是否经过了足够的测试,评价项目的测试水准,评估项目是否达到了高水准测试等级
一个示例,判断是否及格的函数
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
测试结果为:
ok command-line-arguments 0.315s coverage: 66.7% of statements
一共三行代码,只测试了两行,所以覆盖率为66.7%。如果改为如下代码
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
}
此时结果为:
ok command-line-arguments 0.037s coverage: 100.0% of statements
单元测试-依赖
实际功臣中复杂的项目,一般依赖较多,而单元测试需要保证稳定性和幂等性
- 稳定性:相互隔离,能在任何时间、任何环境运行测试
- 幂等性:每一次测试运行都应该产生与之前一样的结果
单元测试-Mock
使用 Monkey 库,Monkey 是一个开源的 mock 测试库,可以对方法或者实例进行 mock。
下面有一个示例,对 ReadFirstLine 函数打桩测试,不在依赖本地文件
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
基准测试
基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。在实际项目开发中经常会遇到代码性能瓶颈,需要利用基准测试对代码做性能分析来定位问题。
- 优化代码,需要对当前代码进行分析
- 内置的测试框架提供了基准测试的能力
下面通过一个例子来说明:随机选择执行服务器,有一个列表存放10个服务器,每次随机选择一个服务器
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i+100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
基准测试以 Benchmark 开头,输入参数为 *testing.B,Parallel 为多协程并发测试
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
运行结果
BenchmarkSelect-16 85675731 13.95 ns/op 0 B/op 0 allocs/op
BenchmarkSelectParallel-16 26753168 44.95 ns/op 0 B/op 0 allocs/op
可以看到代码在并发情况下存在劣化,主要原因是 rand 为了保证全局的随机性和并发安全,持有一把全局锁。为了解决这个问题,使用开源的高性能随机数方法 fastrand
func FastSelect() int {
return ServerIndex[fastrand.Intn(10)]
}
func BenchmarkFastSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
FastSelect()
}
})
}
测试结果:
BenchmarkFastSelectParallel-16 1000000000 0.5244 ns/op 0 B/op 0 allocs/op
可以看到性能提升了百倍,但是需要注意:该函数牺牲了一定的数列一致性,适用大多数场景
总结
本文主要介绍了 Go 单元测试规范、测试 mock 以及基准测试,在实际项目开发中,测试有着不可或缺的作用,应熟练掌握测试的各种方法,避免事故的发生。