0


Golang单元测试、Mock测试以及基准测试

之前参加字节跳动青训营而整理的笔记

Golang拥有一套单元测试和性能测试系统,仅需要添加很少的代码就可以快速测试一段需求代码。

一、单元测试

在这里插入图片描述

单元测试主要包括:输入、测试单元、输出、期望以及与期望的校对。

测试单元包括函数或者结合了一些函数的模块等。我们通过将输出与期望值进行校对,来验证代码的正确性。

通过单元测试,可以一方面保证质量,例如在覆盖率足够的情况下,如果在旧代码中添加了新的代码,通过单元测试可以验证新的代码是否破坏了功能正确性。

另一方面,也提升了效率,例如代码中出现了bug,通过编写单元测试,我们能够在较短的时间内定位或修复问题。

1.1、golang规则

规则1:所有测试文件以

_test.go

结尾。

_test.go

程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;

只有

gotest

会编译所有的程序:普通程序和测试程序。

规则2:测试文件中必须导入

testing

包,并且函数必须写为

func TestXxx(*testing.T)

形式。

例如,某个函数

Add

的测试函数为

TestAdd

,如下所示:

//main.gofuncAdd(a, b int)int{return a + b
}//main_test.gofuncTestAdd(t *testing.T){
   trueOutput :=Add(1,2)
   expectOutput :=3if trueOutput != expectOutput {
      t.Errorf("Expected %v do not match actual %v", expectOutput, trueOutput)}}

规则3:测试的初始化逻辑放到

TestMain

中。

这是一个比较好的用法,

TestMain

函数具体信息如下:

funcTestMain(m *testing.M){//测试前:数据装载、配置初始化等前置工作//...
    
    code := m.Run()//测试后:释放资源等收尾工作//...
    
    os.Exit(code)}

例如:

funcTestMain(m *testing.M){//测试前
    fmt.Println("开始了!")
    run := m.Run()//测试后
    fmt.Println("结束了!")
    os.Exit(run)}funcTestAdd(t *testing.T){
    trueOutput :=Add(1,2)
    expectOutput :=3if trueOutput != expectOutput {
        t.Errorf("Expected %v do not match actual %v", expectOutput, trueOutput)}}//测试结果//开始了!//=== RUN   TestAdd//--- PASS: TestAdd (0.00s)//PASS////结束了!   

1.2、举例&第三方测试包

在该例子中,我们期望

HelloTom

函数返回

“Tom”

,如果返回的不是

“Tom”

则表示测试失败。

很明显,本次测试是失败的。

funcHelloTom()string{return"Jerry"}funcTestHelloTom(t *testing.T){
   output :=HelloTome()
   expectOutput :="Tom"if output != expectOutput {
      t.Errorf("Expected %s do not match actual %s", expectOutput, output)}}//测试结果//=== RUN   TestHelloTom//main_test.go:28: Expected Tom do not match actual Jerry//--- FAIL: TestHelloTom (0.00s)

在单元测试函数中,经常需要编写判断逻辑,我们可以使用一些开源的测试包来帮助简化代码。

例如使用Testift。使用

go get

安装:

go get github.com/stretchr/testify

将上述例子使用

Testify

后,代码如下:

funcTestHelloTom(t *testing.T){
   output :=HelloTom()
   assert.Equal(t,"Tom", output)}

1.3、覆盖率

问题

  • 如何衡量代码是否经过了足够的测试?
  • 如何评价项目的测试水准?
  • 如何评估项目是否达到了高水准测试等级?

我们需要评估单元测试,于是需要引入了单元测试覆盖率。

覆盖率在一定程度上反应了测试用例的覆盖度,越完备那么代码的正确性越有保证。

例子

funcJudgePassLine(score int16)bool{if score >=60{returntrue}returnfalse}funcTestJudgePassLine(t *testing.T){
    isPass :=JudgePassLine(70)
    expectOutput :=trueif expectOutput != isPass {
        t.Errorf("Expected %v do not match actual %v", expectOutput, isPass)}}

使用命令:

go test judgment_test.go judgment.go --cover

结果:

=== RUN   TestJudgePassLine
--- PASS: TestJudgePassLine (0.00s)
PASS

coverage: 40.0% of statements in ./...

如果使用Goland的话,会显示出测试代码的范围。很明显,

JudgePassLine

函数的前两行(例子中第2、3行)已经被验证,而

return false

并没有被验证。

我们可以再写一个分支的单元测试,来提高覆盖率。

funcTestJudgePassLine(t *testing.T){
   isPass :=JudgePassLine(70)
   expectOutput :=trueif expectOutput != isPass {
      t.Errorf("Expected %v do not match actual %v", expectOutput, isPass)}}funcTestJudgePassLineFail(t *testing.T){
   isPass :=JudgePassLine(50)
   expectOutput :=falseif expectOutput != isPass {
      t.Errorf("Expected %v do not match actual %v", expectOutput, isPass)}}//结果//=== RUN   TestJudgePassLine//--- PASS: TestJudgePassLine (0.00s)//=== RUN   TestJudgePassLineFail//--- PASS: TestJudgePassLineFail (0.00s)//PASS////coverage: 60.0% of statements in ./...

从结果可以看出,目前覆盖率已经达到60%了(还有其他函数没有写单元测试)。

当然,在实际项目中,要达到100%的覆盖率是一个可望不可及的目标,一般来说,覆盖率在50%~60%能够认为在一些主流的情况下是没有问题的,但是可能还有有一些异常分支没有覆盖到,对一些例如”提现“等资金类的操作,对覆盖率会要求更高,一般会要求达到覆盖率80%以上。

为了能够提高覆盖率,有一些好的实践:

  1. 测试分支相互独立、全面覆盖。
  2. 测试单元粒度足够小,因此要求函数单一职责。

二、Mock测试

2.1、项目中的依赖

在这里插入图片描述

在一些复杂项目中,会依赖一些数据库、文件或缓存等,这些属于项目的一个强依赖。

单元测试的主要目标有2个:

  1. 幂等。幂等指重复运行一个测试的结果与之前是一致的。
  2. 稳定。指单元测试是能够相互隔离的,单元测试中的函数能在任何时间任何地点独立运行。

如果单元测试中直接调用数据库等外部依赖,那测试是不稳定的,例如:

funcReadFirstLine()string{
   open, err := os.Open("log")defer open.Close()if err !=nil{return""}
   scanner := bufio.NewScanner(open)for scanner.Scan(){return scanner.Text()}return""}funcProcessFirstLine()string{
   line :=ReadFirstLine()
   destLine := strings.ReplaceAll(line,"11","00")return destLine
}//TestfuncTestProcessFirstLine(t *testing.T){
    firstLine :=ProcessFirstLine()
    expectOutput :="line00"if firstLine != expectOutput {
        t.Errorf("Expected %s do not match actual %s", expectOutput, firstLine)}}

从这个例子中可以看出,测试依赖于外部文件,假如外部文件被删除或篡改了,那么这个测试就不可运行了。

因此就需要引入mock机制。

2.2、Mock

常用的开源Mock包monkey:https://github.com/bouk/monkey

该包提供了快速Mock函数:

  • 为一个函数打桩
  • 为一个方法打桩

打桩可以理解为用一个函数A去替换一个函数B,B就是原函数,A就是打桩函数。

例子

将上述读取文件单元测试代码修改,对

ReadFirstLine

打桩测试,使测试不再依赖本地文件。

funcTestProcessFirstLine(t *testing.T){//mock打桩
   monkey.Patch(ReadFirstLine,func()string{return"line00"})defer monkey.Unpatch(ReadFirstLine)//
   firstLine :=ProcessFirstLine()
   expectOutput :="line00"if firstLine != expectOutput {
      t.Errorf("Expected %s do not match actual %s", expectOutput, firstLine)}}

mock在运行时实现,基于go的unsafe包,将内存中函数的地址替换成运行时函数地址。

三、基准测试

go提供了基准测试框架,基准测试是指测试一段程序运行时的性能。

在基准测试中,函数会被调用 N 次(N 是非常大的数,如 N = 1000000),并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。

  • 使用基准测试能够优化代码,当然,这需要对当前代码分析。

例子:

负载均衡例子,随机选择执行服务器

var ServerIndex [10]intfuncInitServerIndex(){for i :=0; i <10; i++{
      ServerIndex[1]= i +100}}funcSelect()int{return ServerIndex[rand.Intn(10)]}//测试//串行的基准测试funcBenchmarkSelect(b *testing.B){InitServerIndex()
    b.ResetTimer()for i :=0; i < b.N; i++{Select()}}//并行的基准测试funcBenchmarkSelectParallel(b *testing.B){InitServerIndex()
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB){for pb.Next(){Select()}})}

测试结果:

BenchmarkSelect-16 180309266(N) 6.596 ns/op(函数执行的平均时间)

BenchmarkSelectParallel-16 29328594 42.33 ns/op

可以看到在并行状态下,性能较为低下,因为Select利用了rand函数,而rand函数为了保证随机性和并发安全,持有一把全局锁,这样就降低了并发性能。

为了提升这个函数的性能,可以用fastrand。

funcBenchmarkFastSelectParallel(b *testing.B){InitServerIndex()
   b.ResetTimer()
   b.RunParallel(func(pb *testing.PB){for pb.Next(){FastSelect()}})}

结果:

BenchmarkFastSelectParallel-16 1000000000 0.5274 ns/op

四、总结

  1. Golang提供了简单而强大的测试工具,而且根据Golang的规则,也使得开发人员能够一眼就明白某个单元测试对应于哪个函数。
  2. 使用第三方单元测试工具包能够简化我们的代码。
  3. 在需要使用到外部依赖的情况下,我们可以利用Mock测试来模拟外部依赖,避免发生不必要的错误。
  4. 基准测试能够得出一段程序的运行性能,便于开发者进行优化,例如上文给出的“随机选择执行服务器”例子。

本文转载自: https://blog.csdn.net/bestzy6/article/details/125515985
版权归原作者 小菜鸡本菜 所有, 如有侵权,请联系我们删除。

“Golang单元测试、Mock测试以及基准测试”的评论:

还没有评论