跳到主要内容
  1. Skills/
  2. 后端编程/

Go 语言中的测试(go Testing)

·字数 3342·7 分钟
go

接触过 java 的同时,junit 这个名称并不会陌生,或者 python 的测试框架 unittest。可是有习惯写测试程序的却聊聊无几。 在 golang 中,关于 testing 的支持比较完善,对于一般的函数测试来说,用不到第三方工具。

官方说明, golang testing

要在 go 程序开发过程中使用单元测试程序,无须安装其它工具,安装了 golang 环境时已经包含,golang 的工具链还是比较完善的。

关于单元测试代码、测试用例、自动化测试等不是有悟想讨论的,不同规模的工程、不同进度的项目要求、不同的管理方法,会让管理者作出不同的决定。本文仅说说如果在 golang 程序中集成可测试代码。

要想在 golang 使用 go test 工具链来实现代码单元测试功能,只需要遵守 go test 工具的规范。

本文并未涉及的 golang testing 的所有内容,只会讲有悟在写测试程序用到的功能,完整内容见上面的官方说明链接。

以下从如何使用的角度来介绍 go testing,并不是大而全的框架性讲解。

按照有悟的惯例,先准备一下示例代码工程:

~/Projects/go/examples
➜ mkdir hellotest && cd hellotest

~/Projects/go/examples/hellotest
➜ go mod init hellotest

~/Projects/go/examples/hellotest
➜ touch youwutoday.go youwu_test.go
// youwutoday.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello testing...😀")
}
// youwu_test.go
package main_test

组织你的单元测试代码 #

  • 测试代码文件名 xxx_test.go,必须遵守。
  • 测试函数签名 TestXXX(t *testing.T),必须遵守。
  • 测试文件所在包,不强制。可以与对应的功能代码文件位于同一个包,开发者根据自己的需要来确定工程规范

为了更好的管理单元测试代码,按照 go testing 的命名规范,运行 go testing 会扫描并编译运行工程目录下 xxx_test.go 代码文件中的 TestXXX 测试函数。对一个没有任何测试代码的 go 程序工程运行 go testing 只会提示 no tests to run 并不会报错。

不用担心这些测试代码管理会让你的代码工程变得混乱,你可以使用一个专门的工程级目录(比如叫 test) 来存放所有主要的测试代码。也可以在对应的包(比如 app)下使用对应测试包(比如 app_test)。这样就可以利用人为区分,用何种目录方式完全取决于你的需要。但这对于 go 工具链来说不是必要的,因为它并不会把这些测试代码打包到程序的发行版本中。

关于 go testing 启动命令 #

在上面的例子中,单元测试用例的启动都是使用 go test 来启动的,关于这个命令,有必要说明的地方。

go test .
仅从当前目录查找 xxx_test.go 测试代码
go test ./...
找出当前的目录以及子目录下所的 xxx_test.go 测试代码
go test ./xxx/xxx
找出路径下的 xxx_test.go 测试代码,如果包含子目录,使用 ./xxx/xxx/...,即在路径后加上 /...,注意,是 3 个 “.” 号。
go test 路径 -v
-v 参数,打开终端信息打印的开关。在没有 -v 选项时,测试代码中的打印函数(t.Logt.Skipt.Errort.Fatal)向终端打印的信息,只有测试函数状态为 FAIL 的才会显示。而 fmt.Print 只会在任一测试函数为 FAIL 时,信息才会显示。所以在调试单元测试代码时,通常会使用 -v 参数,可多次成功执行的,不带 -v,以保持终端信息提示的清晰。这只是实践建议,是否使用取决于你。
go test -run ^TestXXXX$
-run 用来指定执行某个 TestXXX 单元测试函数,支持使用正则表达式来匹配测试函数名称。当然,像 TestMaininit() 这类框架性代码仍然会被执行,以确保环境正确。

在测试中报错与跳出 #

go testing 中,你可能见不到 assertgo testing 框架简化了这一切的工作。在上面的例子,有悟使用了 t.Log 来打印终端信息。你可以使用 t.Error 来结合 if 判断来打印信息,并标记这个测试出现错误。

// youwu_test.go
func TestReportError(t *testing.T) {
    t.Error("report in: t.Error")
}

会得到类似于下面这样的错误报告:

...
=== RUN   TestReportError
    youwu_test.go:42: report in: t.Error
--- FAIL: TestReportError (0.00s)
...
exit status 1
FAIL    hellotest       0.498s

其实 t.Error 等同于 t.Log + t.Fail,当只标记错误不打印信息时,使用t.Fail

跳出单元测试函数的选择:
t.SkipNow
跳出当前单元测试函数,状态为跳出(SKIP,不计为失败),执行下一个测试函数
t.Skip(信息)
t.Log -> t.SkipNow 打印并跳出当前单元测试函数
t.Fail
标记当前单元测试函数状态为错误(FAILED),继续执行
t.FailNow
标记当前单元测试函数状态为错误(FAILED)并跳出,执行下一个测试函数

关于报错机制,有如下几种:

t.Error(信息)
t.Log(信息) -> t.Fail(),该单元测试会继续执行
t.Fatal(信息)
t.Log(信息) -> t.FailNow(),跳出该单元测试,执行下一个单元测试函数

以上关于终端打印的函数 t.Logt.Errort.Fatal 都有对应的格式化形式 t.Logft.Errorft.Fatalf,它们的关系就像 fmt.Printfmt.Printf 的关系一样。

测试程序初始化或清理 #

类似于 java junit 或者 python unittest,单元测试程序的运行分为 SetupRuntearDown三个步骤,其中 setup 是用为 Run 提供环境初始化的过程,比如连接数据库、数据准备等等,tearDown 则是 Run 之后清理,比如删除临时文件、数据库关闭等操作。

go testing 测试框架中并没命名 SetuptearDown 函数。当然,你可以利用 go 程序本身的机制或者 go testing 提供的手段。

当然,如果你的单元测试代码仅仅是一个无须外部数据支持的计算函数,那你根本无须理会初始化或者清理,直接将单元测试代码编写为 xxx_test.goTestXXX 函数即可。

  • 初始化 init 方式

任何 go 代码程序运行时,当前包、当前代码文件中的 init() 函数会被最先执行。xxx_test.go 测试代码的也不例外,那么可以将初始化代码放在这个函数内部。

(一个包内有多个初始化函数时,会按一定顺序执行。将在另一篇文件中介绍 )

youwu_test.go 中添加

package main_test

import (
    "fmt"
    "testing"
)
func TestWorld(t *testing.T) {
    t.Log("TestWorlds")
}
func init() {
    fmt.Println("hello, i'm \"init\".")
}

运行得到的结果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
=== RUN   TestHello
    youwu_test.go:9: TestHello
--- PASS: TestHello (0.00s)
PASS
ok      hellotest       0.316s
  • 清理 有两种级别的清理,一种是对于当前单元测试函数(这非常类似于 defer的延后执行),一种是包级别的。

测试函数级别:使用 t 实例的 Cleanup 方法注册一个在单元测试程序退出前执行的函数。 包级别:使用 TestMain 模式。


func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

运行得到的结果如下:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
=== RUN   TestClean
    youwu_test.go:21: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
...
  • TestMain 方式

TestMaingo testing 测试框架的指定函数。用于控制整个测试过程,这个函数是包级别的。即一个包下,如果有多个 xxx_test.go 测试代码,只能在其中某个 xxx_test.go 中定义。其基本格式大致如下:

在测试代码中添加:

// youwu_test.go
package main_test

import (
    "fmt"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

// func TestWorld(t *testing.T) {
//  t.Log("TestWorlds")
// }

func cleanuptest() {
    fmt.Println("Cleanup.😀")
}
func TestClean(t *testing.T) {
    t.Cleanup(cleanuptest)
    t.Log("testing done.")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在开始运行单元测试代码之前
    // 可以在此处添加环境初始化相关代码或者函数调用
    fmt.Println("😀 开始所有测试前")

    retCode := m.Run()

    // 在全部测试代码运行结束退出之前
    // 可以在此处添加清理代码或函数调用
    fmt.Println("😀 结束所有测试前")

    os.Exit(retCode)
}

使用 go test -v 运行得到:

~/Projects/go/examples/hellotest 
➜  go test -v
hello, i'm "init".
😀 开始所有测试前
=== RUN   TestHello
    youwu_test.go:10: TestHello
--- PASS: TestHello (0.00s)
=== RUN   TestClean
    youwu_test.go:22: testing done.
Cleanup.😀
--- PASS: TestClean (0.00s)
PASS
😀 结束所有测试前
ok      hellotest       0.369s

以上的顺序,可以概括为如下的流程:

`init()` → `TestMain` → TestXXX
                           ↓  
              ↑       ← t.Cleanup

benckmark|性能基准测试 #

go testing 除了函数正确性测试之外,还支持性能基准测试(benchmark),即可多次运行,并计算其平均执行时间从而评估其运行效率。 网上一些开源的 go 项目经常会摆出一些与同类工具的性能基准测试对比,就是使用 go testing benchmark 轻松做出来的。

性能基准测试的代码命名规范与普通 testing 代码的要求类似,除部分性能基准测试专有的函数外,其它像报告打印、跳出机制都一样。不过由于 benchmark 程序会多次执行,除非有必要,在测试函数中尽量少用打印函数。

go 性能基准测试代码的函数命名:

func BenchmarkXxx(*testing.T)

这些基准测试代码可以与普通的单元测试代码放在一起。通常使用 go test 不会进行基准测试,需要添加参数。

还是 youwu_test.go

// youwu_test.go
package main_test

import (
    "fmt"
    "math/rand"
    "os"
    "testing"
)

func TestHello(t *testing.T) {
    t.Log("TestHello")
}

func init() {
    fmt.Println("hello, i'm \"init\".")
}

func TestMain(m *testing.M) {
    // 在开始运行单元测试代码之前
    // 可以在此处添加环境初始化相关代码或者函数调用
    fmt.Println("😀 开始所有测试前")

    retCode := m.Run()

    // 在全部测试代码运行结束退出之前
    // 可以在此处添加清理代码或函数调用
    fmt.Println("😀 结束所有测试前")

    os.Exit(retCode)
}

func BenchmarkRandInt(b *testing.B) {
    for i := 0; i < b.N; i++ {
        rand.Int()
    }
}

使用命令 go test -bench='Rand' -v .得到的结果如下:

~/Projects/go/examples/hellotest 
➜  go test -bench='Rand' -v .
hello, i'm "init".
😀 开始所有测试前
=== RUN   TestHello
    youwu_test.go:19: TestHello
--- PASS: TestHello (0.00s)
goos: darwin
goarch: amd64
pkg: hellotest
cpu: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
BenchmarkRandInt
BenchmarkRandInt-4      70186930                16.21 ns/op
PASS
😀 结束所有测试前
ok      hellotest       1.476s

可见,普通的单元测试代码还是会被执行。使用 -bench='Rand' 参数来指定 go testing 执行 BenchmarkRandInt 这个函数。 终端报告中的

BenchmarkRandInt-4      70186930                16.21 ns/op

表示运行了 70186930 次,每次 16.21 纳秒。更多支持请看官方文档,或者命令 go help test 查看关于 benchmark 部分。