go语言工程实践-测试
测试是开发过程的重要部分,也是软件开发生命周期的关键部分。 它可以确保应用程序正常运行和满足客户需求。测试关系着系统的质量,质量则决定线上系统的稳定性,一旦出现bug漏洞,就会造成事故。
测试是避免事故的最后一道屏障,只要做好完备的测试,就可以避免事故的发生。
测试一般分为以下三种:
- 回归测试:使用APP,如刷抖音看评论
- 集成测试:对功能维度进行自动化测试
- 单元测试:对单独的函数、模块做验证
以上三个:从上到下,测试成本下降,覆盖率上升
下面主要从以下三部分介绍一下测试
- 单元测试
- Mock测试
- 基准测试
单元测试
单元测试主要包括:输入、测试单元、输出、以及校对。
单元的概念比较广,包括接口,函数,模块等。用最后的校对来保证代码的功能与期望相符。
单元测试一方面可以保证质量,在整体覆盖率足够的情况下,一定程度上既保证了新功能本身的正确性,又未破坏原有代码的正确性。另一方面可以提升效率,在代码有bug的情况下,通过编写单元测试,可以在一个短周期内定位和修复问题。
单元测试-规则
- 命名:所有的测试文件以_test.go结尾
- 函数名:
func TestXxx(*testing.T)
Test开头,且连接的第一个字母大写 - 初始化逻辑放到TestMain中:
func TestMain(m *testing.M){
//测试前:数据装载、 配置初始化等
code:=m.Run()
//测试后:释放资源等
os.Exit(code7777776)
}
单元测试-例子1
func HelloTom() string {
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)
}
}
此时,我们的期望和函数输出不符,运行结果如下:
可以看到我们的函数出了问题,那么就要修复这个函数。
单元测试-assert 修复函数
import (
"testing"
"github.com/stretchr/testify/assert"
)
func HelloTom() string {
return "Tom"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
func main() {
testing.Main(func(pat, str string) (bool, error) { return true, nil }, []testing.InternalTest{{"TestHelloTom", TestHelloTom}}, []testing.InternalBenchmark{}, []testing.InternalExample{})
}
运行成功,验证了HelloTom函数的正确。
上述即为单元测试的用法
单元测试-覆盖率
覆盖率就是用来评估单元测试
如图,我们传入score参数为70,在JudgePassLine函数内,if语句和内部语句执行,最后一行返回false没有执行,执行区域占2/3,故输出覆盖率为66.7%。
提升覆盖率
我们可以增加一个不及格的测试case,重新执行所有单元测试,最终覆盖率为100%。
在实际项目,覆盖率为100%是一个期望值,很难达到,覆盖率有以下特点:
- 一般覆盖率:50%~60%,较高覆盖率80%+
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小,函数单一职责。
依赖
工程项目,一般会依赖组件等一些强依赖,组件也很复杂。我们的单元测试需要保证稳定性和幂等性。
- 稳定:指相互隔离,能在任何时间,任何环境,运行测试。
- 幂等:是指每一次测试运行都应该产生与之前一样的结果。
而要实现这一目标就要用到mock机制。
Mock测试
处理文件(如改变数据库)后,可能原本依赖的输入不正确了,程序也就不能运行了。为了避免这个问题,进行Mock打桩。
GoMock是一个Go框架,它与内置的测试包整合得很好,用于解决但原创而是中遇到的外部依赖问题,并且还有Mockgen工具用来辅助生成相关的mock代码。
首先我们需要先安装gomock和mockgen
# 安装gomock和mockgen
go get -u github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen
快速Mock函数:
- 为一个函数打桩
- 为一个方法打桩
就可以理解为,用函数A代替函数B,B是源函数,A就是打桩函数。
Go语言中有很多第三方库提供了 mock 功能,例如Monkey这个库,可以简单地利用Patch将函数的运行地址替换成原来的,最后Unpatch替换回来。保证单元测试的稳定性。
github.com/bouk/monkey 包里面包含两个方法:patch和unpatch
- patch里面参数target表示原函数,replacement代表我们需要打桩的函数
- unpatch表示在打桩后,把这个包卸载掉。
Mockey patch的作用域在runtime,在运行时通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址。
下面就来举例看一下Mock的使用:
import (
"fmt"
"github.com/bouk/monkey"
)
func main() {
// 要替换的函数
originalFunction := func() string {
return "Hello, World!"
}
// 替换函数
patch := monkey.Patch(originalFunction, func() string {
return "Goodbye, World!"
})
defer patch.Unpatch()
fmt.Println(originalFunction())
// 输出: Goodbye, World!
}
在这个例子中,我们使用了 monkey 库中的 Patch 函数来替换 originalFunction 函数,并在最后使用 Unpatch 函数来撤销替换。在这个例子中,替换后的函数将总是返回 “Goodbye, World!”。
基准测试
基准测试是指测试一段程序的运行性能及耗费cpu的程度。 而我们在实际项目开发中会经常遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。 使用方法类似于单元函数,函数以Benchmark开头。
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
如服务器负载均衡中使用到了rand函数,测试时使用串行测试和并行测试效率不同。fastrand函数代替rand函数,发现性能有巨大提升。
//源文件
func Add(a, b int) int {
return a + b
}
//测试文件
import (
"testing"
)
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
测试:在当前目录go文件的目录终端下输入go test -bench=BenchmarkAdd
BenchmarkAdd是测试函数的名称。运行结果如下:
每个基准函数被执行时都有一个不同的b.N值,这个值代表基准函数应该执行的迭代次数。
b.N从1开始,如果基准函数在1秒内就执行完了,那么b.N的值会递增以便基准函数再重新执行(即基准函数默认要运行1秒,如果该函数的执行时间在1秒内就运行完了,那么就递增b.N的值,重新再执行一次)
b.N按照近似顺序增加,每次迭代大约增长20%。基准框架试图更智能,如果它看到较小的b.N值相对较快的完成了迭代,它将b.N增加的更快。
在上面的BenchmarkAdd的例子中,我们发现迭代大约1000000000 次耗时0.748秒,平均每次运行耗时0.1195纳秒。
通过基准测试的结果,我们可以评估代码的性能。我们可以使用不同的输入参数、不同的算法或优化技术来改进代码的性能,并通过基准测试来验证优化效果。