Go-测试 | 青训营笔记

85 阅读5分钟

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

实际项目开发中,还有一个比较重要的概念,就是测试

测试关系着系统的质量,系统质量又决定了线上的稳定性

事故

  1. 营销配置错误,导致非预期用户享受权益,资金损失(损失10w+)
  2. 用户提现,幂等失效,短时间可以多次提现(资金损失20w+)
  3. 代码逻辑错误,广告位被占,无法出广告(收入损失500w+)
  4. 代码指针使用错误,导致APP不可用(损失上kw+)

测试

测试:测试是避免事故的最后一道屏障

只要做好完备的测试,就可以避免事故的发生

测试分为

  • 回归测试:QA(质量保证)手动通过终端回归一些固定的场景(刷抖音、看评论等)
  • 集成测试:系统功能维度的自动化测试(如接口测试)
  • 单元测试: 面向开发阶段,开发者对单独的函数模块进行测试

从上到下,覆盖率逐层变大,成本逐层降低

因此,单元测试的覆盖率,一定程度上决定了产品的质量

单元测试

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

这里的测试单元概念很宽泛,可以指大的聚合函数、模块、接口等

最后通过输出和期望值做校对,来反映我们的代码的运行结果是否和预期相符,是否正确

保证质量: 代码的整体覆盖率足够的情况下,每次编写新代码,加入了单元测试,一方面保证了新功能本身的正确性,由于历史代码也有相关的单元测试,如果整体的单元测试跑通了,又标明了新的代码没有影响原有代码的正确性

提升效率: 代码在有bug的情况下,,通过本地直接运行,可以在短期内,较快地定位错误

规则

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

展开目录可以清晰看到项目的源代码哪些是测试代码

*测试函数命名规范:`func TestXxx(testing.T)

如果命名无误,IDE中会显示可运行的标志

单元测试提供了一个**TestMain函数**,入参函数为*testing.M

在测试之前,可以把初始化的内容(数据装载、配置初始化等前置工作)放入TestMain执行

然后通过m.Run()跑package下的所有单元测试

Run()以后,可以做一些资源释放等的收尾工作,最后os.Exit(code)退出

案例

首先新建一个Package叫Test

写入一个函数,期望返回字符串Jerry

但是实际输出了Tom

package Test

func HelloJerry() string {
	//预期输出Jerry,结果输出了Tom
	return "Tom"
}

同包下新建测试文件

package Test

import "testing"

func TestHelloJerry(t *testing.T) {
	output := HelloJerry()
	exceptOutput := "Jerry"
	if output != exceptOutput {
		t.Errorf("Excepted %s do not match actual %s", exceptOutput, output)
	}
}

可以通过命令行来测试

go test [flags] [packages]

也可以通过IDE进行测试运行

发现问题后需要修复这个函数

package Test

func HelloJerry() string {
	//预期输出Jerry,结果输出了Tom
	return "Jerry"
}

在测试函数进行校对的时候,使用的是不等号(!=)进行判断

除此之外i,有很多开源的算法包,可以实现equal或not equal的比较

下面使用testify的一个算法包来实现

go get github.com/stretchr/testify
package Test

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestHelloJerry(t *testing.T) {
	output := HelloJerry()
	exceptOutput := "Jerry"
	assert.Equal(t, exceptOutput, output)
}

再次测试

覆盖率

如何衡量代码是否经过了足够的测试

如何评价项目的测试水准

如何评估项目是否达到了高水准测试等级

用来评估单元测试的概念,就是代码覆盖率,表示整体上测试用例的覆盖度

定义一个判断成绩是否及格的函数

package cover_rate

func JudgePassLine(score int16) bool {
	if score >= 60 {
		return true
	}
	return false
}

再定义对应的测试函数

package cover_rate

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestJudgePassLine(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

执行命令,测试覆盖率

go test cover.go cover_test.go --cover

可以看出,覆盖率为66.7%

在判断程序中,有三行代码,当输入70的时候,前两条语句已经执行成功,第三条语句并没有执行

因此,覆盖率为:2/3=66.7%

这个时候需要提升覆盖率,也就是程序的完备度

修改程序代码,使第三条语句也可以执行(用例全面覆盖所有语句

package cover_rate

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

func TestJudgePassLine(t *testing.T) {
	isPass := JudgePassLine(70)
	assert.Equal(t, true, isPass)
}

func TestJudgePassLine2(t *testing.T) {
	isPass := JudgePassLine(50)
	assert.Equal(t, false, isPass)
}

Tips

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

    • 50%~60%:基本可以保证主流程没有问题,但异常分支肯定没有覆盖到
    • 资金类交易,覆盖率要求更高,基本80%+,但是覆盖率的提升比较有成本
  • 测试分支相互独立(不重不漏)、全面覆盖

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

依赖

实际项目开发依赖是很复杂的

可能依赖于数据库、文件或者Cache

单元测试中,有两个目标,稳定幂等

幂等:就是每次重复运行一个测试的Case时候,结果都是一样的

稳定性:是指单元测试是相互隔离的,任何单元,任何函数可以在任何函数单独运行

依赖复杂,如果直接写单元测试,如调用数据库、Cache等,可能受网络影响会不稳定

这样在单元测试中就会用到Mock机制