这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
3. 测试
3.1 单元测试
单元测试主要包括、输入、测试单元、输出以及校对,单元的概念比较广,包括接口、函数、模块等;用最后的校对来保证代码的功能与我们的预期相符:
单测一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。
另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。
3.1.1 规则
所有测试文件都以
_test.go结尾
func TestXxx(t *testing.T)
初始化逻辑放到
TestMain里
3.1.2 例子
// hello.go文件中的函数
func HelloTom() string {
// 这里模拟因为业务上的错误,导致最后输出的是错误结果
return "helloJerry"
}
// hello_test.go文件中对应的测试函数
func TestHelloTom() {
// 接收目标测试函数返回的结果
output := HelloTom()
// 期待目标函数返回的结果
expect := "helloTom"
// 如果两者不匹配,直接抛错,并且打印出来这两个结果
if output != expect {
t.Errorf("不匹配,output:%v,expect:%v", output, expect)
}
}
3.1.3 运行
运行测试函数的方法有几种,
- 方式一(
goland):
注意要选中目标文件和测试文件两个,如果只选中一个
Run不会有这种选项。
- 方式二(
Terminal):// 测试文件内的所有函数 go test [flags] [packages] // 想只测试文件中的一个函数 // 前提是当前位置是文件所在的文件夹位置 go test -v -run [函数名称]
这里肯定是测试不通过的,因为两个结果不同,如果把目标函数中的返回结果改正,那么
3.1.4 assert
不用多写代码的测试比较工具——
assert(断言),// 安装工具包 go get -i github.com/stretchr/testify/assert
3.1.5 覆盖率
目前我们了解了如何单元测试,那么如何衡量代码是否经过了足够的测试?如何评价项目的测试水准? 如何评估项目是否达到了高水准测试等级?(致命三连😆)————那就是代码覆盖率
嗯哼,解释一下,我这里这么低的覆盖率,是因为还有很多函数没有测试到,我只测试了
TestScoreCheck这一个函数,如果你文件中只有这一个函数,覆盖率应该是100%。但是如果
ScoreCheck中是if score >= 60 { return true } return false那么覆盖率应该是
66.7%,其实覆盖率就是当前所举用例可以执行目标函数的代码行数的占比
再加个用例,覆盖率就达到百分之百了。
tips:
- 一般的要求是50%~60%覆盖率,而对于资金型服务,覆盖率可能要求达到80%+
- 我们做单元测试,测试分支相互独立、全面覆盖
- 要求函数体足够小,这样就比较简单的提升覆盖率,也符合函数设计的单一职责
3.2 文件处理
// read.go中的目标函数
func ReadFirstLine() string {
// 操作文件,可能需要提前在文件中添加一写数据
// line11
// line22
// line33
// line44
open, err := os.Open("log.txt")
// 最后关闭文件,释放资源
defer open.Close()
// 如果有异常,返回空字符串
if err != nil {
return ""
}
// 创建扫描器
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
// 读取第一行数据
line := ReadFirstLine()
// 将数据中的“11”全部替换为“00”
destLine := strings.ReplaceAll(line, "11", "00")
// 返回修改后的数据
return destLine
}
// read_test.go中的测试函数
func TestProcess(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
3.3 Mock
这里我们用了Monkey,monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock, 反射,指针赋值。Mockey Patch的作用域在Runtime,在运行时通过通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,将待打桩函数或方法的实现跳转到。
// 下载monkey包 go get -i github.com/bouk/monkey
func TestWithMock(t *testing.T) {
// 对ReadFirstLine打桩测试,不再依赖本地文件
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
// 释放资源
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
// 比较结果
assert.Equal(t, "line000", line)
}
3.4 基准测试
Go 语言还提供了基准测试框架,基准测试是指测试一段程序的运行性能及耗费CPU的程度,而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试,使用方法类似于单元测试,
3.4.1 例子
// jizhun.go文件函数详情
// 模拟随机选择执行服务器
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)]
}
// jizhun_test.go文件函数详情
// 基准测试为benchmark,所以是*testing.B
func BenchmarkSelect(b *testing.B) {
// 初始化数据
InitServerIndex()
// 重置因为初始化耗费的时间,这些操作不应该作为基准测试的范围
b.ResetTimer()
// 对一个测试用例的默认测试时间是1秒,当测试用例函数返回时还不到1秒,那么 testing.B中的N值将按1、2、5、10、20、50...递增,并以递增后的值重新进行用例函数测试
for i := 0; i < b.N; i++ {
Select()
}
}
// runparallel是多协程并发测试,执行2个基准测试,发现代码在并发情况下存在劣化,主要原因是rand为了保证全局的随机性和并发安全,持有了一把全局锁。
func BenchmarkSelectParallel(b *testing.B) {
// 初始化
InitServerIndex()
// 重置时间
b.ResetTimer()
// 多协程并发测试
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Select()
}
})
}
3.4.2 优化
为了解决这一随机性能问题,字节开源了一个高性能脑加数方法
fastrand,再做一下基准测试,性能提升了百倍。主要的思路是牺牲了一定的数列一致性,在大多数场合是适合的,如果在后面遇到随机的场景可以尝试用一下。go get -i github.com/bytedance/gopkg
未完待续。。。