go测试基础 | 青训营笔记

106 阅读4分钟

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

今天和大家分享如何对go程序进行测试

单元测试

单元测试概念

基本概念:

单元测试是面对开发过程进行的测试,测试对象是对开发过程中的相应函数模块进行测试。

单元测试通过输入相应参数进入测试单元,对输出值和期望值进行校对,来完成对函数和模块的测试。

单元测试是测试成本最低但同时需要更高覆盖率的测试。

测试规则:

  • 所有测试文件以_test.go结尾

  • 函数命名和参数模板为:func TestXxx(*testing.T)。这里的testing是go语言的内置包。每一个测试函数都可以独立运行。

  • 初始化逻辑放到TestMain函数中:

    import "testing"
    func TestMain(m *testing.M) {
        // 测试前:数据装载,配置初始化等前置工作
        code := m.Run()
        ...
        // 测试后:释放资源等收尾工作
        os.Exit(code)
    }
    

测试运行:

运行单元测试,我们可以使用go test [flags] [packages],当然,更常用的是使用IDE自带的运行测试,逐个测试函数运行等按钮。

测试辅助:

我们可以使用assert包来辅助验证,

例子:

import (
    "github.com/stretchr/testify/assert"
    "testing"
)
func TestEqual(t *testing.T) {
    output := 1
    expectOutput := 1
    assert.Equal(t, expectOutput, output)
}

其他参考资料:

Go Test 单元测试简明教程 | 快速入门 | 极客兔兔 (geektutu.com)

测试覆盖率:

代码覆盖率是对整体程序可靠程度的重要评估标准。

计算代码测试的覆盖率可以使用go test xx_test.go xx.go --cover命令,在进行测试的同时就可以得到测试程序对xx.go的测试覆盖率是多少:

// xx.go
func JudgePassLine(score int16) bool {
    if score >= 60 {
        return true
    }
    return false
}
// xx_test.go
func TestJudgePassLine(t *testing.T) {
    isPass := JudgePassLine(70) // 引入待测试函数
    assert.Equal(t, true, isPass)
}

得到以下覆盖率数据:

ok      command-line-arguments  0.665s  coverage: 66.7% of statements

说明其代码覆盖率达到了66.7%。

虽然我们在测试函数中执行了JudgePassLine函数,但是由于我们只验证了70得到true的情况,所以并没有对函数测试完全。我们修改测试函数(或新增测试函数):

func TestJudgePassLine(t *testing.T) {
    isPass := JudgePassLine(70) // 引入待测试函数
    assert.Equal(t, true, isPass)
    notPass := JudgePassLine(30)
    assert.Equal(t, false, notPass)
}

重新执行命令,得到以下数据:

ok      command-line-arguments  0.687s  coverage: 100.0% of statements

表明我们已经对xx.go中的函数做到了完全覆盖。

在实际项目中,我们对需要测试的单元,通常达到50%-60%即可;需要高度测试的单元,我们可以尽量做到80%以上。

mock测试

在日常的项目开发中,一般都会存在很多依赖,例如gorm,gin,os等,我们在使用这些依赖进行项目开发时,对每个模块都进行单元测试会很麻烦,此时我们可以采取mock测试

我们使用gomonkey测试包进行mock测试,这是一个常用的mock测试包。示例如下:

import (
    "bou.ke/monkey"
    "github.com/stretchr/testify/assert"
    "testing"
)
func TestFnxxx(t *testing.T) {
    // 对Fnxxx进行打桩
    monkey.Patch(Fnxxx, func() bool {
        return true
    })
    defer monkey.Unpatch(Fnxxx)
    ...
}

上面示例中提到的打桩,就是将某个函数A替换成打桩函数P.

打桩函数的意义在于:若A函数的使用复杂,返回不规律且可能受各种环境影响不稳定,但是B函数(待测试函数)需要使用到函数A,那么我们就可以使用一个打桩函数P来替换函数A。打桩函数通过可控的返回来测试B。

实例:

// xx.go
// 复杂函数
func FetchSomeApi() string {
    // ...一系列fetch操作
}
// 待测试函数
func ProcessFetchSomeApi() string {
    str := FetchSomeApi()
    return strings.ReplaceAll(str, "a", "b")
}
​
// xx_test.go
func TestProcessFetchSomeApi(t *testing.T) {
    monkey.Patch(FetchSomeApi, func() string {
        return "abc"
    })
    defer monkey.Unpatch(FetchSomeApi)
    strProcessed := ProcessFetchSomeApi()
    assert.Equal(t, "bbc", strProcessed)
}

注意这里进行测试运行的时候,由于golong使用了内部优化,导致打桩会失效。我们可以使用命令行进行测试:

go test xx_test.go xx.go -gcflags=all=-l

基准测试

基准测试是测试一段程序来查看cpu的损耗,我们通常对程序进行基准测试来分析程序性能,来找到瓶颈和优化点。

基准测试和单元测试规则相似,其命名规则为BenchmarkXxx

例如,我们对上面单元测试的JudgePassLine函数进行基准测试:

func BenchmarkJudgePassLine(b *testing.B) {
    // 重置操作,在重置操作之前,我们可以执行一些其他准备函数,不会记录在性能中
    b.ResetTimer()
    // 注意for循环使用b.N来模拟大量的循环触发
    for i := 0; i < b.N; i++ {
        JudgePassLine(70)
    }
}

得到测试结果:

BenchmarkJudgePassLine-16       1000000000               0.2906 ns/op

表明执行了1000000000次花费了0.2906ns。

上述的循环操作是串行的,我们可以使用并行来重新测试:

func BenchmarkJudgePassLine(b *testing.B) {
    // 重置操作,在重置操作之前,我们可以执行一些其他准备函数,不会记录在性能中
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            JudgePassLine(70)
        }
    })
}

得到测试结果:

BenchmarkJudgePassLine-16       100000000               18.00 ns/op

可以看出,当程序比较简单的时候,其实使用串行比并行更加有效率。