这是我参与「第三届青训营 -后端场」笔记创作活动的的第4篇笔记
总结一下 Go 工程实践中的测试环节
常见事故
- 营销配置错误,导致非预期用户享受权益
- 用户提现,幂等失效,短时间可以多次提现,资金损失
- 代码逻辑错误,广告位被占,无法出广告
- 代码指针使用错误,导致App不可用
任何一种错误都容易带来资金损失,那么我们就需要测试来尽量避免损失。
测试
测试主要有三种:
-
回归测试
-
集成测试
-
单元测试
从上到下,覆盖率逐层变大,成本逐层降低。
下面主要对单元测试进行总结
单元测试
以下面函数为例:
func Hello() string {
return "Ben"
}
所有测试文件以_test.go结尾
func TestXxx(*testing.T)
func TestHello(t *testing.T) {
output := Hello()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match %s", expectOutput, output)
}
}
初始化逻辑放到TestMain中
func TestMain(m *testing.M) {
//测试前:数据装载,配置初始化等前置工作
code := m.Run
//测试后:释放资源等收尾工作
os.Exit(code)
}
完整代码
//hello.go
package test
func Hello() string {
return "Ben"
}
//Hello_test.go
package test
import (
"testing"
)
func TestHello(t *testing.T) {
output := Hello()
expectOutput := "Tom"
if output != expectOutput {
t.Errorf("Expected %s do not match %s", expectOutput, output)
}
}
运行结果
--- FAIL: TestHello (0.00s)
Hello_test.go:14: Expected Tom do not match Ben
FAIL
exit status 1
FAIL Hello_test 0.001s
assert
assert可以让我们更方便地测试代码。使用如下:
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestHello(t *testing.T) {
output := Hello()
expectOutput := "Tom"
assert.Equal(t, expectOutput, output)
}
覆盖率
覆盖率用途:
- 衡量代码是否经过了足够的测试
- 评价项目的测试水准
- 评估项目是否达到了高水准测试等级
go test Hello_test.go hello.go --cover
评估:
- 一般覆盖率:50%~60%,对于资金型服务可能要求较高覆盖率80%+;
- 测试分支相互独立、全面覆盖;
- 测试单元粒度足够小,函数单一职责
tips:
可以用 -coverprofile 参数,将详细的结果输出到文件中。
比如 go test -coverprofile cover.out 便是将覆盖测试的结果输出到 cover.out 中。然后我们用命令 go tool cover -html=cover.out -o cover.html 将测试结果输出为 html 文件,再用浏览器打开便可以看到哪些代码被单元测试覆盖到了,哪些没有被覆盖到。其中绿色部分表示已覆盖到的代码,红色部分表示没有覆盖到。hello函数测试如下:
Mock
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
}
func TestProcessFirstLine(t *testing.T) {
firstLine := ProcessFirstLine()
assert.Equal(t, "line00", firstLine)
}
文件log
line11
line22
line33
将文件中的第一行字符串中的11替换成00,执行单测,单测需要依赖本地文件,如果文件被修改或者删除测试就会fail,为了保证稳定性,我们对读取文件函数进行mock,屏蔽对于文件的依赖
Monkey是一个开源的mock库,可以对method,或者实例的方法进行mock、反射、指针赋值
monkey: github.com/bouk/monkey
Monkey Patch的作用域在Runtime,运行时通过Go的unsafe包,能够将内存中的函数的地址替换为运行时函数的地址,将待打桩函数的方法实现跳转到。
//Patch replaces a function with another
func Patch(target, replacement interface{}) *PatchGuard {
t := reflect.ValueOf(target)
r := reflect.ValueOf(replacement)
patchValue(t, r)
return &PatchGuard{t, r}
}
//Unpatch removes any monkey patches on target
//return whether target was patched in the first place
func UnPatch(target interface{}) bool {
return unpatchValue(reflect.ValueOf(target))
}
使用:
func TestProcessFirstLineWithMock(t *testing.T) {
monkey,Patch(ReadFirstLine, func() string {
return "line110"
})
defer mokey.Unpatch(ReadFirstLine)
line := ProcessFirstLine()
assert.Equal(t, "line000", line)
}
基准测试
基准测试属于性能测试,通常用于对具体的功能函数做性能分析,比如加密算法函数。基准测试需要有对比测试,以便衡量不同代码实现之间的性能差异,从中选取性能最好的实现方式。
- 优化代码,需要对当前代码分析
- 内置的测试框架提供了基准测试的能力
这里举一个服务器负载均衡的例子,首先有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)]
}
基准测试以Benchmark开头,入参是 *testing.B 用b中的N值反复循环测试。
go test -bench=.
-
参数
-bench,它指明要测试的函数;点字符意思是测试当前所有以Benchmark为前缀函数 -
参数
-benchmem,性能测试的时候显示测试函数的内存分配大小,内存分配次数的统计信息 -
参数
-count n,运行测试和性能多少此,默认一次 (对一个测试用例的默认测试时间是1秒,当测试用例函数返回时还不到一秒,那么testing.B中的N值将按1、2、5、10、20、50......递增,并以递增后的值重新进行用例函数测试。)
我们在reset之前做了init或其他准备工作,可以使用 b.ResetTimer(),重置计时为0,runparallel是多协程并发测试;执行两个基准测试,发现在并发情况下存在劣化,主要原因是rand为了保证全局的随机性和并发安全,持有了一把全局锁。
字节为了解决这一随机性能问题,开源了一个高性能随机数方法fastrand,性能提升了百倍(主要思路是牺牲一定的数列一致性,在大多数场景下都适用)