参加字节青训营的对单元测试的笔记。
测试这一块,对后端开发其实还是挺重要的,之前春招也被问到过。
在Go语言中,单元测试使用gotest命令。
在包目录中,以_test.go结尾的源代码就是gotest的一部分。
_test.go有三种类型,分别是测试函数、基准函数、示例函数、mock测试等。
测试函数的函数名前缀是Test,目的是测试程序的逻辑性。
基准函数的函数名前缀是Benchmark,目的是测试程序的性能。
示例函数的函数名前缀是Example,目的是写一些示例文档。
mock测试主要借助于gostub、GoConvey、gomonkey等mock框架。目的是对依赖的数据进行解耦合,能够依托于自身条件进行测试(换而言之就是模拟出一个假的数据)。
先来看简单的两个测试:
import (
"github.com/stretchr/testify/assert"
"testing"
)
func HelloTom() string {
return "Tom"
}
func TestHelloTom(t *testing.T) {
output := HelloTom()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
func TestHelloJerry(t *testing.T) {
output := HelloTom()
expectOutput := "Jerry"
assert.Equal(t, expectOutput, output)
}
对其分别test运行:
可以看到测试结果分别是成功或者是失败,而失败也会有具体的错误信息与、预期结果。
为什么需要检测覆盖率?
接下来简单编写一个测试覆盖率的函数方法:
用于判断分数是否在60分及以上:
func JudgePassLine(score int16) bool {
if score >= 60 {
return true
}
return false
}
编写一个case进行测试:
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
}
go test judgment.go judgment_test.go --cover
进行测试:
覆盖率只达到了66.7%,因为case中的分数小于60,而没有大于等于60的情况。为了让覆盖率达到100%我们可以再加一个case。
func TestJudgePassLineTrue(t *testing.T) {
isPass := JudgePassLine(70)
assert.Equal(t, true, isPass)
}
func TestJudgePassLineFail(t *testing.T) {
isPass := JudgePassLine(50)
assert.Equal(t, false, isPass)
}
但是只是这样无法很高的提高覆盖率,因为只是体现的百分比。
接下来介绍一个可以可视化体现覆盖率的方法:
go test judgment.go judgment_test.go --cover -coverprofile=c.out
使用以上的命令生成覆盖率输出文件,再通过go tool生成可视化html结果。
go tool cover -html=c
基准测试主要的目的就是为了测试已有函数,优化执行性能。
且go内置的测试框架,提供了进行基准测试的能力。
基本参数说明:
- -run 用于单次测试,一般用于代码逻辑验证
- -bench=. 执行所有 Benchmark,也可以通过用例函数名来指定部分测试用例
- -benchtime 指定测试执行时长
- -cpuprofile 输出 cpu 的 pprof 信息文件
- -memprofile 输出 heap 的 pprof 信息文件。
- -blockprofile 阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置
- -mutexprofile 互斥锁分析,报告互斥锁的竞争情况
其基本形式为:
func BenchmarkXXX(b *testing.B){
// ...
}
这里举一个服务器负载均衡的例子,首先我们有10个服务器列表,每次随机执行select函数随机选择一个执行。
import "math/rand"
var ServerIndex [10]int
func InitServerIndex() {
for i := 0; i < 10 ; i++ {
ServerIndex[i] = i+100
}
}
func Select() int {
return ServerIndex[rand.Intn(10)]
}
编写对应的测试函数:
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()
}
})
}
接下来使用字节的一个开源的fastrand进行优化:
import "github.com/bytedance/gopkg/lang/fastrand"
var ServerIndex [10]int
func FastSelect() int {
return ServerIndex[fastrand.Intn(10)]
}
再进行基准测试:
func BenchmarkFastSelectSelectParallel(b *testing.B) {
InitServerIndex()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next(){
FastSelect()
}
})
}
可以发现这个速度算是快了三十多倍。主要的思路是牺牲了一定的数列一致性,再大多数场景是适用的,可以自己尝试一下。
这次介绍主要用monkey框架进行打桩(mock)。
首先了解下什么是打桩?
monkey能够实现什么?
- 支持为一个函数打一个桩
- 支持为一个成员方法打一个桩
- 支持为一个接口打一个桩
- 支持为一个全局变量打一个桩
- 支持为一个函数变量打一个桩
- 支持为一个函数打一个特定的桩序列
- 支持为一个成员方法打一个特定的桩序列
- 支持为一个函数变量打一个特定的桩序列
- 支持为一个接口打一个特定的桩序列
- IO类型的,本地文件,数据库,网络API,RPC等
- 依赖的服务还没有开发好,这时候我们自己可以模拟一个服务,加快开发进度提升开发效率
- 压力性能测试的时候屏蔽外部依赖,专注测试本模块
- 依赖的内部函数非常复杂,要构造数据非常不方便,这也是一种
接下来进行一个简单的case进行测试:
import (
"bufio"
"os"
"strings"
)
func ReadFirstLine() string {
open, err := os.Open("log")
defer open.Close()
if err != nil {
return ""
}
scanner := bufio.NewScanner(open)
for scanner.Scan() {
return scanner.Text()
}
return ""
}
func ProcessFirstLine() string {
line := ReadFirstLine()
destLine := strings.ReplaceAll(line, "11", "00")
return destLine
}
打开log文件将第1行的11换成00。并在如下编写不同测试函数(正常/mock)
import (
"bou.ke/monkey"
"github.com/stretchr/testify/assert"
"testing"
)
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
func TestProcessFirstLineWithMock(t *testing.T) {
monkey.Patch(ReadFirstLine, func() string {
return "line110"
})
defer monkey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
自己可以去试试去把文件名改一改,发现两个如果把log名字改了第一个会发生IO错误,而进行了打桩的函数却不会。
说明成功对依赖的文件进行了解耦合。
分别总结测试函数、Mock测试、基准测试的业务场景,与简单应用,虽然很多人不是专门的侧开岗,但是拥有一定的测试能力,对于代码的质量,以及查错的能力也会有一定的提升。