这是我参与「第五届青训营」伴学笔记创作活动的第 6 天
实际项目开发中,还有一个比较重要的概念,就是测试
测试关系着系统的质量,系统质量又决定了线上的稳定性
事故
- 营销配置错误,导致非预期用户享受权益,资金损失(损失10w+)
- 用户提现,幂等失效,短时间可以多次提现(资金损失20w+)
- 代码逻辑错误,广告位被占,无法出广告(收入损失500w+)
- 代码指针使用错误,导致APP不可用(损失上kw+)
测试
测试:测试是避免事故的最后一道屏障
只要做好完备的测试,就可以避免事故的发生
测试分为
- 回归测试:QA(质量保证)手动通过终端回归一些固定的场景(刷抖音、看评论等)
- 集成测试:系统功能维度的自动化测试(如接口测试)
- 单元测试: 面向开发阶段,开发者对单独的函数模块进行测试
从上到下,覆盖率逐层变大,成本逐层降低
因此,单元测试的覆盖率,一定程度上决定了产品的质量
单元测试
主要包括:输入、测试单元、输出,以及与期望输出的校对
这里的测试单元概念很宽泛,可以指大的聚合函数、模块、接口等
最后通过输出和期望值做校对,来反映我们的代码的运行结果是否和预期相符,是否正确
保证质量: 代码的整体覆盖率足够的情况下,每次编写新代码,加入了单元测试,一方面保证了新功能本身的正确性,由于历史代码也有相关的单元测试,如果整体的单元测试跑通了,又标明了新的代码没有影响原有代码的正确性
提升效率: 代码在有bug的情况下,,通过本地直接运行,可以在短期内,较快地定位错误
规则
所有测试文件以_test.go结尾
展开目录可以清晰看到项目的源代码哪些是测试代码
*测试函数命名规范:`func TestXxx(testing.T)
如果命名无误,IDE中会显示可运行的标志
单元测试提供了一个**TestMain函数**,入参函数为*testing.M
在测试之前,可以把初始化的内容(数据装载、配置初始化等前置工作)放入TestMain执行
然后通过m.Run()跑package下的所有单元测试
Run()以后,可以做一些资源释放等的收尾工作,最后os.Exit(code)退出
案例
首先新建一个Package叫Test
写入一个函数,期望返回字符串Jerry
但是实际输出了Tom
package Test
func HelloJerry() string {
//预期输出Jerry,结果输出了Tom
return "Tom"
}
同包下新建测试文件
package Test
import "testing"
func TestHelloJerry(t *testing.T) {
output := HelloJerry()
exceptOutput := "Jerry"
if output != exceptOutput {
t.Errorf("Excepted %s do not match actual %s", exceptOutput, output)
}
}
可以通过命令行来测试
go test [flags] [packages]
也可以通过IDE进行测试运行
发现问题后需要修复这个函数
package Test
func HelloJerry() string {
//预期输出Jerry,结果输出了Tom
return "Jerry"
}
在测试函数进行校对的时候,使用的是不等号(!=)进行判断
除此之外i,有很多开源的算法包,可以实现equal或not equal的比较
下面使用testify的一个算法包来实现
go get github.com/stretchr/testify
package Test
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestHelloJerry(t *testing.T) {
output := HelloJerry()
exceptOutput := "Jerry"
assert.Equal(t, exceptOutput, output)
}
再次测试
覆盖率
如何衡量代码是否经过了足够的测试
如何评价项目的测试水准
如何评估项目是否达到了高水准测试等级
用来评估单元测试的概念,就是代码覆盖率,表示整体上测试用例的覆盖度
定义一个判断成绩是否及格的函数
package cover_rate
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
再定义对应的测试函数
package cover_rate
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
执行命令,测试覆盖率
go test cover.go cover_test.go --cover
可以看出,覆盖率为66.7%
在判断程序中,有三行代码,当输入70的时候,前两条语句已经执行成功,第三条语句并没有执行
因此,覆盖率为:2/3=66.7%
这个时候需要提升覆盖率,也就是程序的完备度
修改程序代码,使第三条语句也可以执行(用例全面覆盖所有语句)
package cover_rate
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestJudgePassLine(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
func TestJudgePassLine2(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
}
Tips
-
一般覆盖率:50%~60%,较高覆盖率80%+
- 50%~60%:基本可以保证主流程没有问题,但异常分支肯定没有覆盖到
- 资金类交易,覆盖率要求更高,基本80%+,但是覆盖率的提升比较有成本
-
测试分支相互独立(不重不漏)、全面覆盖
-
测试单元粒度足够小,函数单一职责
依赖
实际项目开发依赖是很复杂的
可能依赖于数据库、文件或者Cache
单元测试中,有两个目标,稳定和幂等
幂等:就是每次重复运行一个测试的Case时候,结果都是一样的
稳定性:是指单元测试是相互隔离的,任何单元,任何函数可以在任何函数单独运行
依赖复杂,如果直接写单元测试,如调用数据库、Cache等,可能受网络影响会不稳定
这样在单元测试中就会用到Mock机制