这是我参与「第五届青训营 」笔记创作活动的第四天
一、本堂课重点内容:
- 单元测试
- Mock测试
- 基准测试
二、详细知识点介绍:
一个系统的质量的重要保障就是测试,测试关系着系统的质量,质量则决定线上系统的稳定性,一但出现bug漏洞,就会造成事故
一门成熟的开发语言必然有一套成熟的测试方法,Go也不例外,如果在日常开发的过程中,能够做好完备的测试,我就可以大大减少事故发生的可能性
测试按覆盖范围可以分为回归测试、集成测试、单元测试
回归测试一般是手动通过终端回归一些固定的主流程场景
集成测试是对系统功能维度做测试验证
而单元测试测试开发阶段,开发者对单独的函数、模块做功能验证
层级从上至下,测试成本逐渐减低,而测试覆盖率逐步上升,所以单元测试的覆盖率一定程度上决定这段代码的质量。
单元测试
单元测试主要包括,输入,测试单元,输出,以及校对
单元的概念比较广,包括接口,函数,模块等;用最后的校对来保证代码的功能与我们的预期相符
单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单测,可以在一个较短周期内定位和修复问题。
在Go语言中,单元测试主要遵循以下规范
- 所有测试文件以
_test.go结尾func TestXxx(*testing.T)- 初始化逻辑放到
TestMain中 以下是例子
//文件名:helloTom.go
//需要测试代码
func HelloTom() string {
return "Jerry"
}
//文件名:helloTom_test.go
//测试的代码
import "testing"
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
if output!=expectOutput {
t.Errorf("Expected %s do not match actual %s",expectOutput,output)
}
}
运行结果:
将代码修改之后,以覆盖率运行:
//待测试代码
func HelloTom(i int) string {
if i==1 {
return "Jerry"
}
return "Tom"
}
func TestHelloTom(t *testing.T) {
output := HelloTom(1)
expectOutput := "Tom"
//if output!=expectOutput {
// t.Errorf("Expected %s do not match actual %s",expectOutput,output)
//}
assert.Equal(t,expectOutput,output)
}
因为是输入1,只走了if中的语句,所以达到了0.7的覆盖率
在实际项目中,一般的要求是50%~60%覆盖率,而对于资金型服务,覆盖率可能要求达到80%;我们做单元测试,测试分支相互独立、全面覆盖,测试单元粒度足够小,函数体足够小,这样就比较简单的提升覆盖率,也符合函数设计的单一职责。
工程中复杂的项目,一般会依赖许许多多的外部依赖,而我们的单元测试需要保证稳定性和幂等性
稳定是指相互隔离,能在任何时间,任何环境,运行测试
幂等是指每一次测试运行都应该产生与之前一样的结果。
Mock测试
在平时的开发中,不可避免的会遇到一种情况,就是会接收前端收到的数据或者是接收文件,并且通过文件来运行这个程序,但是在测试过程中用平常的单元测试会非常的麻烦
这种情况我们就可以使用打桩(mock)测试
这个是不用mock测试的时候
//待测试的代码
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return "文件读取出错了!\n"
}
scan := bufio.NewScanner(open)
for scan.Scan() {
return scan.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11","00")
return destLine
}
//log文件
line11
line22
line33
line44
line55
//测试代码
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t,"line00",firstLine)
}
很明显的是,这个测试代码是依赖于log文件的
现在我们来打个桩试试,在此过程中我们需要这个网站找到依赖
我们只要修改一下测试代码就好
func TestProcessFirstLine(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string{
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
firstLine := ProcessFirstLine()
assert.Equal(t,"line000",firstLine)
}
在此过程中,我们可以发现这个桩直接返回了一个值,从而不需要log文件也可以测试
以下是
Patch()与Unpatch()的源码
基准测试
Go 语言还提供了基准测试框架
基准测试是指测试一段程序的运行性能及耗费 CPU 的程度。
而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试,其使用方法类似于单元测试
//待测试代码
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
ServerIndex[i] = i+100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
//测试代码
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()
}
})
}
测试结果:
基准测试以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[fastrand.Intn(10)]
}
func BenchmarkSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
FastSelect()
}
})
}
测试结果:
可以看到,有一个很明显的提升!
三、课后个人总结:
通过这次学习我了解到了许多关于测试的知识
以前,其实我一般对于保障项目质量都是不断的运行,一遍一遍的造数据
但是这样其实是很没有保障的,通过这次学习,我会尝试去使用单元测试等一系列科学的测试方法来保障我的项目运行可靠性
今天同样是收获满满呀!