Go语言工程实践入门(二)测试 | 青训营

35 阅读5分钟

测试

单元测试

在开发的过程中可以单独对一个函数或者模块进行单元测试,以降低最终项目整体集成测试的成本。

在 Go 语言中,主要使用go test进行测试,加上-v选项能够看到每一个测试函数的结果,为了使用这个命令进行测试,测试程序需要遵循以下特征。

  1. 测试代码的文件名需为xxx_test.go
  2. 测试用例函数名需为TestXxx
  3. 测试函数用例类型为func TestXxx(t *testing.T),单元测试需通过它反馈测试结果;

TestMain 函数

初始化逻辑和结尾逻辑需要放到TestMain中,TestMain 的参数是 *testing.M 类型。

func TestMain(m *testing.M) {
    setup()
    code := m.Run()
    teardown()
    os.Exit(code)
}

如果测试文件中有TestMain函数,生成测试时将调用该函数,而不是直接进行测试,调用m.Run()将触发所有的测试用例的执行,使用os.Exit()处理返回的状态码,不为 0 表示测试用例失败

一般将测试前的实例化等准备工作写在func setup() {}函数中,将指针释放,网络关闭等收尾工作写在func teardown() {}函数中。

错误输出

testing.T 中支持的可导出方法如下,通过t.Method()调用

// 获取测试名称
method (*T) Name() string
// 打印日志
method (*T) Log(args ...interface{})
// 打印日志,支持 Printf 格式化打印
method (*T) Logf(format string, args ...interface{})
// 反馈测试失败,但不退出测试,继续执行
method (*T) Fail()
// 反馈测试失败,立刻退出测试
method (*T) FailNow()
// 反馈测试失败,打印错误
method (*T) Error(args ...interface{})
// 反馈测试失败,打印错误,支持 Printf 的格式化规则
method (*T) Errorf(format string, args ...interface{})
// 检测是否已经发生过错误
method (*T) Failed() bool
// 相当于 Error + FailNow,表示这是非常严重的错误,打印信息结束需立刻退出。
method (*T) Fatal(args ...interface{})
// 相当于 Errorf + FailNow,与 Fatal 类似,区别在于支持 Printf 格式化打印信息;
method (*T) Fatalf(format string, args ...interface{})
// 跳出测试,从调用 SkipNow 退出,如果之前有错误依然提示测试报错
method (*T) SkipNow()
// 相当于 Log 和 SkipNow 的组合
method (*T) Skip(args ...interface{})
// 与 Skip 类似,相当于 Logf 和 SkipNow 的组合,区别在于支持 Printf 格式化打印
method (*T) Skipf(format string, args ...interface{})
// 用于标记调用函数为 helper 函数,打印文件信息或日志,不会追溯该函数。
method (*T) Helper()
// 标记测试函数可并行执行,这个并行执行仅仅指的是与其他测试函数并行,相同测试不会并行。
method (*T) Parallel()
// 可用于执行子测试
method (*T) Run(name string, f func(t *T)) bool

此外还有一个 package 可以实现测试,下面是一个例子,引入github.com/stretchr/testify/assert包,assert.Equal(t, expectOutput, output)进行测试。

表组测试

表组测试可以避免重复编写大量的测试代码,通过定义一个结构体数组进行测试结果的遍历,如下面这个例子

type TestTable struct {
    a         float64 // 输入参数
    b         float64 // 输入参数
    expect    float64 // 期待结果
    expectErr error   // 期待错误字符串
}

通过定义结构体数组测试多组情况

var table = []TestTable{
    {...},
    {...},
    {...},
}

通过编写测试函数遍历上述数组即可测试多种情况。

进行指定测试

  • 直接运行go test,该 package 下所有测试函数都会被执行

-run选项接受正则表达式参数,可以选择部分测试函数进行测试。

  • 测试一个具体的测试函数
    go test -run "^TestFunction$"
    
  • 测试某一类函数,如测试所有带有Function名的函数
    go test -run "Function"
    
  • 测试具体的文件
    go test xxx_test.go xxx.go
    

子测试

在同一个测试函数里面测试多个子测试场景,使用Run()方法进行子测试

func TestMul(t *testing.T) {
    t.Run("pos", func(t *testing.T) {
        if Mul(2, 3) != 6 {
            t.Fatal("fail")
        }
    })
    
    t.Run("neg", func(t *testing.T) {
        if Mul(2, -3) != -6 {
            t.Fatal("fail")
        }
    })
}

帮助函数

对于某些重复的逻辑,可以抽取出来作为帮助函数,为了输出调用帮助函数者的信息,而不是被调用的帮助函数内部的信息(这样不知道具体问题在哪个测试,因为错误都发生在帮助函数内部),需要标明这是帮助函数,增加语句t.Helper(),这样当测试失败时,报告的错误位置将指向调用 t.Run 的位置而不是 t.Helper() 的位置

type calcCase struct{ A, B, Expected int }

func createMulTestCase(c *calcCase, t *testing.T) {
    t.Helper()
    t.Run(c.Name, func(t *testing.T) {
        if ans := Mul(c.A, c.B); ans != c.Expected {
            t.Fatalf("%d * %d expected %d, but %d got",
                c.A, c.B, c.Expected, ans)
        }
    })
}

然后就可以直接调用这个帮助函数进行多次测试。

单元测试覆盖率

使用--cover选项可以查看测试用例执行函数的比例,即测试的覆盖率,对于一般的项目,覆盖率需要达到 50% ~ 60%

Mock

当部分函数需要使用外部依赖时,比如需要依赖数据库里的数据,但是数据可能被篡改,或者不稳定,因此使用 Mock 打桩进行替换测试

monkey是一个开源的mock测试库,可以对method,或者实例的方法进行mock,地址为https://github.com/bouk/monkey

monkey的Patch可以将目标函数或模块替换为临时的函数,实际上是替换了运行时的函数地址,Unpatch方法可以恢复原目标函数

monkey.Patch(ReadFirstLine, func() string {
    return "line110"
})
defer monkey.Unpatch(ReadFirstLine)

上面这个例子将ReadFirstLine进行了打桩测试,本来需要从数据库中读取数据,替换的函数可以直接返回希望的结果。

基准测试

基准测试可以测试函数的性能,func BenchmarkName(b *testing.B) {},函数名必须以Benchmark开头,参数为b *testing.Bb *testing.B,在执行测试时需要-bench选项

如果在运行前基准测试需要一些耗时的配置,则可以使用 b.ResetTimer() 先重置定时器,只计时需要查看性能的部分

使用 RunParallel 测试并发性能

func BenchmarkSelectParallel(b *testing.B) {
    b.ResetTimer()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
        Select()
    }
}