“这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
测试关系着系统的质量,质量则决定线上系统的稳定性,一旦出现bug漏洞,就会造成事故。只要做好完备的测试,就可以避免事故的发生。 在实际工程开发中,还有一个重要概念——单元测试。
本节分为四个部分:
- 测试:
一些分类 - 单元测试:
它的组成部分,它的测试指标,它的简单实现,go test运行 - 单元测试——assert和mock
当单元测试调用到数据库(DB)或CPU缓存(Cache),稳定性受到破坏,不能再仅使用assert比较实际输出和预期输出,需要加上mock机制。 - 基准测试
1. 测试
按开发阶段分类可以分为以下四种类型:
- 回归测试: 质量保证人员手动通过终端回归一些固定的主流程场景
- 集成测试:对系统功能维度做自动化测试验证
- 单元测试:开发阶段开发者对单独的函数、模块做功能验证。单元测试的覆盖率一定程度上决定代码质量。
性能测试:其中一种是基准测试
2. 单元测试
组成部分
单元概念:包括接口、函数、模块等
- 保证质量:在整体覆盖率足够的情况下,保证新功能本身正确性的同时,又未破坏原有代码的正确性。(原有代码本身就有自己的测试,加了功能后再对整体测试,若测试通过即可保证质量。)
- 提升效率:在代码有bug情况下通过编写单测可以在较短周期内定位和修复问题。
单元测试指标-覆盖率
-
覆盖率是什么:
描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。已经测试过的代码的行数/整个行数。(当遇到分支时就会造成有的代码不会被执行测试到) -
覆盖率作用:
衡量代码是否经过足够测试, 评价项目的测试水准, 评估项目是否达到了高水准测试等级。 -
一般覆盖率50%~60%:可以保证主流程没有问题,有些异常分支可能没有覆盖到。
较高覆盖率80%:资金类交易要求。 -
如何提升覆盖率?
- 测试分支相互独立、全面覆盖。
- 测试单元粒度足够小(函数体小),函数单一职责。
单元测试实现-写法规则:
- 测试文件和源文件放在一块,测试文件命名为
源文件名_test.go - 测试函数取名为TestXxx。
- 测试函数参数有且只有一个:
- 对普通函数测试参数为:
t *testing.T - 对main函数测试参数为:
t *testing.M - 基准测试参数是 :
t *testing.B
- 对普通函数测试参数为:
- 初始化逻辑放到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
- Patch: 参数(原函数,打桩函数),返回类型*PatchGuard
- Unpatch:参数(原函数),返回类型bool,作用:在测试接收后将桩卸载掉
实现例子
左图为原文件,右图为测试文件,包括了assert测试和加上mock机制后的测试。
4. 基准测试
基准测试是指测试一段程序的运行性能及耗费CPU的程度。 在实际开发中经常遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。
- 运行: (包需为根文件)
go test -bench="." \\ 测试文件中所有函数都会运行
go test -bench="BenchmarkXxx" \\ 只运行测试函数BenchmarkXxx
也可以再添加-run xxx_test.go指定运行测试文件.
以下是一个简单例子:
左图为原文件,构建了一个大小为10的全局数组ServerIndex.
InitServerIndex(): 用来初始化该全局数组。
Select() : 使用rand随机从数组中取一个值。
FastSelect() : 使用fastrand随机从数组中取一个值。
rand函数和fastrand函数区别在于:fastrand通过牺牲了一定的随机数列的一致性,在并发时有着更高的效率.
右图为测试文件
运行结果如下, 可以发现并发测试Select函数反而比循环测试Select函数花费时间更多了,原因在于rand为了保证全局随机性持有了一把全局锁,所以在并发时会导致效率反而降低了. 可以通过fastrand优化,达到我们想要的并发效果.
函数-------------------执行次数-----------平均每次执行时间
参考
[1] 手把手教你如何进行 Golang 单元测试 - 知乎 (zhihu.com)
[2] Go Test 单元测试简明教程 | 快速入门 | 极客兔兔 (geektutu.com)
[3] Golang 基准测试(Benchmark)-阿里云开发者社区 (aliyun.com)
[4] blog.csdn.net/dpl12/artic…
[5] Go 语言工程实践之测试 - 掘金 (juejin.cn)