这是我参与「第五届青训营」伴学笔记创作活动的第 4 天
测试是系统开发中很重要的一环,关乎程序部署上线以后能否正常运行。测试分为回归测试、集成测试和单元测试,覆盖率依次变大,测试成本逐渐降低。
- 回归测试主要针对整个系统的整体的所有功能进行测试。
- 集成测试主要针对其中某一个功能进行测试。
- 单元测试主要针对一个测试单元,可能是一个函数或一个模块进行测试。
因此,覆盖率最大的单元测试的质量在一定程度上决定了代码的质量,Golang 内置了测试框架用于进行单元测试。
单元测试
单元测试主要是对单独的一个函数或一个模块等测试单元进行测试,校对结果是否符合期望,从而判断测试单元是否存在错误。单元测试包含以下内容: Golang 单元测试的规则:
基本使用
- 测试单元:一个函数
- 测试文件:
- 测试函数:
- 使用
go test
命令运行:
- 修改了错误,测试正确:
覆盖率
覆盖率是一项指标,用于衡量代码是否经过了足够的测试,评价项目的测试水准以及评估项目是否达到了高水准测试等级。 例如:以如下这个函数作为测试单元。
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
测试函数:
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
if !isPass {
t.Errorf("Has error!")
}
}
运行测试时加上--cover
参数:
go test unittest/service_test.go unittest/service.go --cover
可以看到本次测试的覆盖率为
75%
。 因为测试函数执行以后,被测试函数只执行了score >= 60
的分支,即只执行了 3 行代码,而整个被测试函数共有 4 行代码,所以覆盖率为 .
为提高覆盖率,增加一个测试函数:
func TestJudgePassLineFalse(t *testing.T) {
isPass := JudgePassLine(50)
if isPass {
t.Errorf("Has error!")
}
}
实际项目中:
Mock
实际项目中某个函数或模块的返回结果通常会依赖于文件、数据库、缓存等外部依赖,因此每次测试的结果可能会不一样。而单元测试要求具有幂等性和稳定性,即要求每次测试都返回同样的结果。因而在测试的时候,可以采用 Mock 的方式来构造数据,代替从数据库等外部依赖中获取数据,从而保证每次测试都返回相同结果,保证测试的幂等与稳定。
例如:
待测试函数依赖于文件log.txt
:
// 读取文件的第一行
func ReadFirstLine() string {
open, err := os.Open("log.txt")
defer open.Close() // 函数结束时调用,关闭资源
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
// 待测试函数
func UpperFirstLine() string {
res := ReadFirstLine()
return strings.ToUpper(res)
}
使用如下测试函数的话,一旦文件内容改变,测试结果也将改变:
func TestUpperFirstLine(t *testing.T) {
res := UpperFirstLine()
if res != "HELLO" {
t.Errorf("res isn't HELLO but %v", res)
}
}
因而可以采用 Mock 。
Mock 常用的第三方库:https://github.com/bouk/monkey
func TestUpperFirstLineWithMock(t *testing.T) {
// 打桩,替换内存中的 ReadFirstLine 函数
monkey.Patch(ReadFirstLine, func() string {
return "hello"
})
// 结束后要换回来
defer monkey.Unpatch(ReadFirstLine)
res := UpperFirstLine()
if res != "HELLO" {
t.Errorf("res isn't HELLO but %v", res)
}
}
这样即可保证测试时不依赖外部文件,从而保证测试的稳定性和幂等性。
基准测试
基准测试为测试一段程序运行时的性能与 CPU 的损耗。基准测试可进行代码分析,结果可用于优化代码。Golang 内置的测试框架也提供了基准测试的能力。
package unittest
import "math/rand"
var serverIndex [10]int
func InitServerIndex() {
for i := 0; i < 10; i++ {
serverIndex[i] = i + 100
}
}
// 基准测试的被测试函数为 Select 函数
func SelectServer() int {
return serverIndex[rand.Intn(10)]
}
测试函数:基准测试的测试函数以Benchmark
开头。
package unittest
import "testing"
func BenchmarkSelect(b *testing.B) {
InitServerIndex()
b.ResetTimer() // 下面才是真正开始测试 SelectServer 函数,所以要重置定时器时间
for i := 0; i < b.N; i++ {
SelectServer()
}
}
测试结果:
结果分析:这里主要是因为
rand.Intn
函数为了保证全局的随机性和并发安全而在并行时持有全局锁,所以并行时性能会劣化。