这是我参与「第五届青训营」伴学笔记创作活动的第 10 天
Go语言工程实践之测试
本节课主要讲解go测试相关的内容,包括单测规范,测试mock,以及基准测试。
介绍
测试一般分为三种类型——回归测试、集成测试、单元测试。
- 回归测试:QA(质量保证工程师)同学手动通过终端回归一些固定的主流程场景
- 集成测试:对系统功能维度做测试验证
- 单元测试:开发者对单独的函数、模块做功能验证 三类测试从上到下,覆盖率逐层变大,成本却逐层降低。
单元测试
测试主要包括输入、输出、校对,单元包括接口、函数、模块。
规则
- 所有测试文件以_test.go结尾
- func TestXxx(*testing.T)
- 初始化逻辑放到TestMain中
// publish_post.go
// publish_post_test.go
func TestMain(m *testing.M){
// 测试前:数据装载,配置初始化等前置工作
code := m.Run()
// 测试后:释放资源等收尾工作
os.Exit(code)
}
样例
func HelloTom() {
return "Jerry"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match actual %s", expectOutput, output)
}
}
运行
go test [flags][packages]
assert包
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
func HelloTom() string {
return "Tom"
}
覆盖率
我们通过代码覆盖率来对单元测试进行评估
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)
}
运行go test judgment_test.go judgment.go --cover
返回ok command-line-arguments 1.296s coverage: 66.7% of statements
上面的示例覆盖率为66.7%,我们可以添加一个不及格的测试case,重新执行所有单元测试,最终覆盖率为100%。
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
}
同样运行go test judgment_test.go judgment.go --cover
返回ok command-line-arguments (cached) coverage: 100.0% of statements
Tips
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖
- 测试单元粒度足够小,函数单一职责
Mock
monkey: github.com/bouk/monkey 是一个开源的mock包 快速Mock(v.模仿)函数
- 为一个函数打桩
- 为一个方法打桩
- 打桩可以理解为用一个函数A(打桩函数)替换函数B(原函数)
// Patch replaces a function with another
func Patch(target, replacement interface{}) *PathcGuard {
t := reflect.ValueOf(target)
r := reflect.Valueof(replacement)
patchValue(t, r)
return &PathcGuard{t, r}
}
Mockey Patch的作用域在Runtime,在运行时通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,实现待打桩函数或方法的跳转。
Mock样例
下面是一个mock的使用样例,通过patch对Readfineline进行打桩mock,默认返回line110,这里通过defer卸载mock,这样整个测试函数就摆脱了本地文件的束缚和依赖。
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
基准测试
Go语言还提供了基准测试框架,基准测试是指测试一段程序的运行性能以及耗费CPU的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试。
例子
import (
"math/rand"
)
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i<10; i++ {
ServerIndex[i] = i+100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
这是一个服务器负载均衡的例子,首先我们有10个服务器列表,每次随机执行select函数随机选择一个执行
运行
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer()
for i := 0; i < b.N; i++ {
Select()
}
}
// 18.77 ns/op
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
// 79.42 ns/op
基准测试以Benchmark开头,入参是testing.B,用b中的N值反复递增循环测试 (对一个测试用例的默认测试时间是1秒,当测试用例函数返回时还不到1秒,那么testing.B 中的N值将按1、 2、5、10、20、50....递增,并以递增后的值重新进行用例函数测试。) Resttimer重置计时器,我们再reset之前做了init或其他的准备操作,这些操作不应该作为基准测试的范围; runparallel是多协程并发测试;执行2个基准测试,发现代码在并发情况下存在劣化,主要原因是rand为了保证全 局的随机性和并发安全,持有了一把全局锁。
优化
公司为了解决这一随机性问题,开源了一个高性能随机数方法fastrand。
func FastSelect() int {
return ServerIndex[fastran.Intn(10)]
}
重新进行基准测试后性能提升百倍(0.6951ns/op),在后面遇到随机的场景可以尝试用一下。