Go 工程实践之测试 | 青训营笔记

132 阅读5分钟

49928ef3e3054f3cb43848a0e4d577ed_tplv-k3u1fbpfcp-zoom-crop-mark_1304_1304_1304_734.webp 这是我参与「第三届青训营 -后端场」笔记创作活动的的第4篇笔记

总结一下 Go 工程实践中的测试环节

常见事故

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

任何一种错误都容易带来资金损失,那么我们就需要测试来尽量避免损失。

测试

测试主要有三种:

  • 回归测试

  • 集成测试

  • 单元测试

屏幕截图 2022-06-12 165225.png 从上到下,覆盖率逐层变大,成本逐层降低。

下面主要对单元测试进行总结

单元测试

以下面函数为例:

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

评估:

  1. 一般覆盖率:50%~60%,对于资金型服务可能要求较高覆盖率80%+;
  2. 测试分支相互独立、全面覆盖;
  3. 测试单元粒度足够小,函数单一职责

tips:

可以用 -coverprofile 参数,将详细的结果输出到文件中。

比如 go test -coverprofile cover.out 便是将覆盖测试的结果输出到 cover.out 中。然后我们用命令 go tool cover -html=cover.out -o cover.html 将测试结果输出为 html 文件,再用浏览器打开便可以看到哪些代码被单元测试覆盖到了,哪些没有被覆盖到。其中绿色部分表示已覆盖到的代码,红色部分表示没有覆盖到。hello函数测试如下:

屏幕截图 2022-06-12 231108.png

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,性能提升了百倍(主要思路是牺牲一定的数列一致性,在大多数场景下都适用)