Go进阶-测试详解 | 青训营笔记

144 阅读6分钟

“这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天

测试关系着系统的质量,质量则决定线上系统的稳定性,一旦出现bug漏洞,就会造成事故。只要做好完备的测试,就可以避免事故的发生。 在实际工程开发中,还有一个重要概念——单元测试。

本节分为四个部分:

  1. 测试:
    一些分类
  2. 单元测试:
    它的组成部分,它的测试指标,它的简单实现,go test运行
  3. 单元测试——assert和mock
    当单元测试调用到数据库(DB)或CPU缓存(Cache),稳定性受到破坏,不能再仅使用assert比较实际输出和预期输出,需要加上mock机制。
  4. 基准测试

1. 测试

按开发阶段分类可以分为以下四种类型: 1675479022(1).png

  • 回归测试: 质量保证人员手动通过终端回归一些固定的主流程场景
  • 集成测试:对系统功能维度做自动化测试验证
  • 单元测试:开发阶段开发者对单独的函数、模块做功能验证。单元测试的覆盖率一定程度上决定代码质量。

性能测试:其中一种是基准测试

2. 单元测试

组成部分

1675483520(1).png 单元概念:包括接口、函数、模块等

  • 保证质量:在整体覆盖率足够的情况下,保证新功能本身正确性的同时,又未破坏原有代码的正确性。(原有代码本身就有自己的测试,加了功能后再对整体测试,若测试通过即可保证质量。)
  • 提升效率:在代码有bug情况下通过编写单测可以在较短周期内定位和修复问题。

单元测试指标-覆盖率

  • 覆盖率是什么:
    描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。已经测试过的代码的行数/整个行数。(当遇到分支时就会造成有的代码不会被执行测试到)

  • 覆盖率作用:
    衡量代码是否经过足够测试, 评价项目的测试水准, 评估项目是否达到了高水准测试等级。

  • 一般覆盖率50%~60%:可以保证主流程没有问题,有些异常分支可能没有覆盖到。
    较高覆盖率80%:资金类交易要求。

  • 如何提升覆盖率?

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

单元测试实现-写法规则:

  1. 测试文件和源文件放在一块,测试文件命名为源文件名_test.go
  2. 测试函数取名为TestXxx。
  3. 测试函数参数有且只有一个:
    • 对普通函数测试参数为:t *testing.T
    • 对main函数测试参数为:t *testing.M
    • 基准测试参数是 :t *testing.B
  4. 初始化逻辑放到TestMain中
     func TestMain(m *testing.M) {
     
         // 测试前:数据装载、配置初始化等前置工作
         
         code:=m.Run() // 跑这个包下的所有单元测试
         
         // 测试后:释放资源等收尾工作
         
         os.Exit(code)
     }
         
    

单元测试实现-例子

// print.go 文件 
    package test
    func HelloTom() string {
	return "Tom"
    }

// print_test.go 文件
    package test    
    import (
	"github.com/stretchr/testify/assert"
	"testing"
    )
    func TestHelloTom(t *testing.T) {
	output := HelloTom()
	expectOutput := "Tom"
	assert.Equal(t, expectOutput, output)
    }

go test运行

go test:运行该包下的所有测试用例
go test -v -cover-v显示每个用例测试结果,-cover查看覆盖率
go test -run TestAdd -v:只运行该包下的一个测试用例并显示测试结果

3. 单元测试——assert和mock

单元测试分为两类:

  • 对于无第三方依赖的纯逻辑代码,只需验证相关逻辑,使用assert(断言),通过控制输入输出对比结果即可。
  • 对于有第三方依赖的代码,需要将相关依赖mock(模拟)之后,才能通过assert验证相关逻辑。这里需要借助第三方工具库处理 单元测试实践出发 单元测试只是针对单个函数的测试,关注其内部逻辑,对于网络/数据库访问等需要通过相应手段进行mock。

assert
开源包"github.com/stretchr/testify/assert" 作用:比较预期输出和实际输出

mock
工程中复杂的项目,一般会依赖第三方库,单元测试需要保证稳定性和幂等性,当单元测试调用到数据库(DB)或CPU缓存(Cache),可能会依赖一些网络就会造成不稳定,用到mock机制解决。

稳定:相互隔离,能在任何时间任何环境运行测试。
幂等:每一次测试运行都应该产生与之前一样的结果。

当测试单元存在IO操作, 依赖本地文件时,如果更改or删除该本地文件,单元测试就会失败。为了单元测试稳定性,我们对读取文件函数进行mock,屏蔽对文件的依赖。它可以实现对实例的方法进行mock(打桩)。

  • 打桩理解:用一个函数A去替换另一个函数B,则A称为打桩函数,B为原函数。

  • 实现机制:mock的实现主要是在运行时通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,这样最终在测试时调用的是打桩函数,实现了mock功能。这样就通过mock实现了不依赖本地的测试。

  • 使用方法

选择开源包"github.com/bouk/monkey"
通过该包的两个方法:Patch和Unpatch

  1. Patch: 参数(原函数,打桩函数),返回类型*PatchGuard
  2. Unpatch:参数(原函数),返回类型bool,作用:在测试接收后将桩卸载掉

实现例子
左图为原文件,右图为测试文件,包括了assert测试和加上mock机制后的测试。 单元测试.png

4. 基准测试

基准测试是指测试一段程序的运行性能及耗费CPU的程度。 在实际开发中经常遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。

  • 运行: (包需为根文件)
    go test -bench="." \\ 测试文件中所有函数都会运行
    go test -bench="BenchmarkXxx" \\ 只运行测试函数BenchmarkXxx
    也可以再添加-run xxx_test.go指定运行测试文件.

以下是一个简单例子:

左图为原文件,构建了一个大小为10的全局数组ServerIndex.
InitServerIndex(): 用来初始化该全局数组。
Select() : 使用rand随机从数组中取一个值。
FastSelect() : 使用fastrand随机从数组中取一个值。

rand函数和fastrand函数区别在于:fastrand通过牺牲了一定的随机数列的一致性,在并发时有着更高的效率.

右图为测试文件 基准测试.png

运行结果如下, 可以发现并发测试Select函数反而比循环测试Select函数花费时间更多了,原因在于rand为了保证全局随机性持有了一把全局锁,所以在并发时会导致效率反而降低了. 可以通过fastrand优化,达到我们想要的并发效果.

函数-------------------执行次数-----------平均每次执行时间 1675498042(1).png

参考

[1] 手把手教你如何进行 Golang 单元测试 - 知乎 (zhihu.com)
[2] Go Test 单元测试简明教程 | 快速入门 | 极客兔兔 (geektutu.com)
[3] Golang 基准测试(Benchmark)-阿里云开发者社区 (aliyun.com) [4] blog.csdn.net/dpl12/artic…
[5] Go 语言工程实践之测试 - 掘金 (juejin.cn)