Go语言项目实战--测试|青训营笔记

167 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记

1. 前言

承接上篇笔记,本篇主要介绍有关Go语言项目实战--测试方面的内容。主要涉及到单元测试,Mock测试与基准测试,通过对项目测试的了解,进一步加深对GO语言项目实战的理解与实践。

2. 测试

Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。

  • go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。

  • 在*_test.go文件中有三种类型的函数,单元测试函数、Mock测试函数和基准测试函数。

3. 单元测试

项目开发完毕一定要做单元测试,保证代码质量

image.png

规则

  • 所有测试文件以_test.go结尾

    • 通过展开目录就可以清晰地分辨项目的源代码和测试代码,方便定位
  • func TestXxx(*testing.T)

    • 测试函数命名规范

    • IDE中如果命名不规范,会出现提醒标志

  • 初始化逻辑放到TestMain中

    • 入参是*testing.M,可以在测试之前实现一些数据装载、配置初始化等前置工作都可以在TestMain中执行,是在包维度生效的

测试之后,进行释放资源等收尾工作

assert

实现一些equalnotEqual的操作

覆盖率

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

可以通过代码覆盖率来评估

代码覆盖率在一定程度上表明我们测试用例的覆盖度,越完备表示对代码的正确性就越能表示

Tips

  • 一般覆盖率:50% ~·60%;较高覆盖率80%+

    • 一定程度上可以保证主流程没问题,但是一场分支可能没有覆盖到

    • 对于资金类交易:支付等业务,覆盖率要求更高,要求到80%+

    • 从60到80的提升是比较有成本的

  • 测试分支相互独立、全面覆盖

    • 为了更好的提高覆盖率,首先测试分支需要是完备的

    • 基于分支能够保证测试的全面覆盖

  • 测试单元粒度足够小,函数单一职责

    • 还需要保证单元测试的粒度足够小,这就需要函数足够小,这与代码设计的单一职责相对应

4. Mock测试

背景

image.png

  • 项目可能依赖数据库、cache(redis类似的组件)或本地的文件,这些都属于项目的强依赖

  • 单测目标:

    • 幂等:重复运行测试的case结果与之前一样
    • 稳定性:单侧能够相互隔离,单元测试能在任何时间、任何函数独立运行

仅仅写单元测试调用DB、cache,那么它的测试测试可能会因为依赖网络变得不稳定。所以会用到Mock机制,以保证代码的稳定性

文件处理

如果文件被篡改,可能是个测试文件,被别人随便删除了。这样测试在特定场景下就难以运行了

MOCK测试

  • 开源的Mock测试包——monkey:github.com/bouk/monkey

  • 实现对method以及实例的打桩(打桩:用A函数替换函数 B,B原函数,B是打桩函数)

  • 快速Mock函数

    • 为一个函数打桩
    • 为一个方法打桩
  • 以函数打桩为例,有两个方法:

    • Patch

      // Patch replaces a function with another
      func Patch(target, replacement interface{}) *PatchGuard {
         t := reflect.ValueOf(target)
         r := reflect.ValueOf(replacement)
         patchValue(t, r)
      ​
         return &PatchGuard{t, r}
      }
      
      • target:原函数,目标替换的函数
      • replacement:需要打桩的函数
    • Unpath打桩结束需要卸载

      为了保证测试结束之后,把桩卸载掉

      // Unpatch removes any monkey patches on target
      // returns whether target was patched in the first place
      func Unpatch(target interface{}) bool {
         return unpatchValue(reflect.ValueOf(target))
      }
      

关于Mock的实现,主要是在运行时通过Go的Unsafe包,将内存当中的地址替换为运行时函数的地址,最终测试时调用的就是打桩函数,一次实现Mock的功能

5. 基准测试

基准测试主要是通过测试 CPU 和内存的效率问题,来评估被测试代码的性能,进而找到更好的解决方案。比如链接池的数量不是越多越好,那么哪个值才是最优值呢,这就需要配合基准测试不断调优了。

有时候通过本地进行性能测试会比较方便

  • 优化代码,需要对当前代码分析

    • 实际开发中经常会遇到热点代码或代码性能瓶颈问题,为了定位问题,就需要对代码进行性能分析,使用方法类似于单元测试
  • 内置的测试框架提供了基准测试的能力

    • Go还提供了基准测试框架,以测试一段程序的性能和CPU的损耗。

运行

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()
      }
   })
}
// 并行
func BenchmarkFastSelectParallel(b *testing.B) {
   InitServerIndex()
   b.ResetTimer()
   b.RunParallel(func(pb *testing.PB) {
      for pb.Next() {
         FastSelect()
      }
   })
}

并行的基准测试,其性能有劣化。问题在于,select用到了SDK的random函数,该函数为了保证全局的随机性和并发安全,持有一把全局锁,这样在一定程度长降低了并发性能。在并行情况下,性能减低。

优化

为了解决随机函数的性能问题,Github开源了fastrand函数,性能提升了百倍。但是在高并发场景下,fastrand如果不是也别注意底层实现的话,可能会出现一些性能问题。

后记

本篇笔记是参加「第三届青训营 -后端场」课程后创作,这里介绍了Go语言项目测试的相关内容。其中,单元测试是自己平时项目开放中做的不到位的地方,通过此次课程了学习不仅加什么对项目测试的理解,同时也为自己后续的项目开发增加了经验。