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

160 阅读4分钟

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

Go语言工程实践之测试

本节课主要讲解go测试相关的内容,包括单测规范,测试mock,以及基准测试。

介绍

测试一般分为三种类型——回归测试、集成测试、单元测试。

  • 回归测试:QA(质量保证工程师)同学手动通过终端回归一些固定的主流程场景
  • 集成测试:对系统功能维度做测试验证
  • 单元测试:开发者对单独的函数、模块做功能验证 三类测试从上到下,覆盖率逐层变大,成本却逐层降低。

单元测试

测试主要包括输入、输出、校对,单元包括接口、函数、模块。

规则

  • 所有测试文件以_test.go结尾
  • func TestXxx(*testing.T)
  • 初始化逻辑放到TestMain中
// publish_post.go
// publish_post_test.go
func TestMain(m *testing.M){
    // 测试前:数据装载,配置初始化等前置工作
    code := m.Run()
    // 测试后:释放资源等收尾工作
    os.Exit(code)
}

样例

func HelloTom() {
    return "Jerry"
}

func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    if output != expectOutput {
        t.Errorf("Expected %s do not match actual %s", expectOutput, output)
    }
}

运行

go test [flags][packages]

assert包

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

func TestHelloTom(t *testing.T) {
    output := HelloTom()
    expectOutput := "Tom"
    assert.Equal(t, expectOutput, output)
}

func HelloTom() string {
    return "Tom"
}

覆盖率

我们通过代码覆盖率来对单元测试进行评估

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

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

运行go test judgment_test.go judgment.go --cover 返回ok command-line-arguments 1.296s coverage: 66.7% of statements

上面的示例覆盖率为66.7%,我们可以添加一个不及格的测试case,重新执行所有单元测试,最终覆盖率为100%。

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

同样运行go test judgment_test.go judgment.go --cover 返回ok command-line-arguments (cached) coverage: 100.0% of statements

Tips

  • 一般覆盖率:50%~60%,较高覆盖率80%+
  • 测试分支相互独立、全面覆盖
  • 测试单元粒度足够小,函数单一职责

Mock

monkey: github.com/bouk/monkey 是一个开源的mock包 快速Mock(v.模仿)函数

  • 为一个函数打桩
  • 为一个方法打桩
  • 打桩可以理解为用一个函数A(打桩函数)替换函数B(原函数)
// Patch replaces a function with another
func Patch(target, replacement interface{}) *PathcGuard {
    t := reflect.ValueOf(target)
    r := reflect.Valueof(replacement)
    patchValue(t, r)
    return &PathcGuard{t, r}
}

Mockey Patch的作用域在Runtime,在运行时通过Go的unsafe包,能够将内存中函数的地址替换为运行时函数的地址,实现待打桩函数或方法的跳转。

Mock样例

下面是一个mock的使用样例,通过patch对Readfineline进行打桩mock,默认返回line110,这里通过defer卸载mock,这样整个测试函数就摆脱了本地文件的束缚和依赖。

func TestProcessFirstLineWithMock(t *testing.T) {
    monkey.Patch(ReadFirstLine, func() string {
        return "line110"
    })
    defer monkey.Unpatch(ReadFirstLine)
    line := ProcessFirstLine()
    assert.Equal(t, "line000", line)

基准测试

Go语言还提供了基准测试框架,基准测试是指测试一段程序的运行性能以及耗费CPU的程度。而我们在实际项目开发中,经常会遇到代码性能瓶颈,为了定位问题经常要对代码做性能分析,这就用到了基准测试。使用方法类似于单元测试。

例子

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)]
}

这是一个服务器负载均衡的例子,首先我们有10个服务器列表,每次随机执行select函数随机选择一个执行

运行

func BenchmarkSelect(b *testing.B) {
    InitServerIndex()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        Select()
    }
}
// 18.77 ns/op
func BenchmarkSelectParallel(b *testing.B) {
    InitServerIndex()
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            Select()
        }
    })
}
// 79.42 ns/op

基准测试以Benchmark开头,入参是testing.B,用b中的N值反复递增循环测试 (对一个测试用例的默认测试时间是1秒,当测试用例函数返回时还不到1秒,那么testing.B 中的N值将按1、 2、5、10、20、50....递增,并以递增后的值重新进行用例函数测试。) Resttimer重置计时器,我们再reset之前做了init或其他的准备操作,这些操作不应该作为基准测试的范围; runparallel是多协程并发测试;执行2个基准测试,发现代码在并发情况下存在劣化,主要原因是rand为了保证全 局的随机性和并发安全,持有了一把全局锁。

优化

公司为了解决这一随机性问题,开源了一个高性能随机数方法fastrand。

func FastSelect() int {
    return ServerIndex[fastran.Intn(10)]
}

重新进行基准测试后性能提升百倍(0.6951ns/op),在后面遇到随机的场景可以尝试用一下。