Go 语言中的测试(go Testing)
目录
接触过 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.Log
、t.Skip
、t.Error
、t.Fatal
)向终端打印的信息,只有测试函数状态为 FAIL 的才会显示。而fmt.Print
只会在任一测试函数为 FAIL 时,信息才会显示。所以在调试单元测试代码时,通常会使用-v
参数,可多次成功执行的,不带-v
,以保持终端信息提示的清晰。这只是实践建议,是否使用取决于你。go test -run ^TestXXXX$
-run
用来指定执行某个TestXXX
单元测试函数,支持使用正则表达式来匹配测试函数名称。当然,像TestMain
、init()
这类框架性代码仍然会被执行,以确保环境正确。
在测试中报错与跳出 #
在 go testing 中,你可能见不到 assert
。go 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.Log
、t.Error
、t.Fatal
都有对应的格式化形式 t.Logf
、t.Errorf
、t.Fatalf
,它们的关系就像 fmt.Print
与 fmt.Printf
的关系一样。
测试程序初始化或清理 #
类似于 java junit 或者 python unittest,单元测试程序的运行分为 Setup
、Run
、tearDown
三个步骤,其中 setup
是用为 Run
提供环境初始化的过程,比如连接数据库、数据准备等等,tearDown
则是 Run
之后清理,比如删除临时文件、数据库关闭等操作。
在 go testing 测试框架中并没命名 Setup
、tearDown
函数。当然,你可以利用 go 程序本身的机制或者 go testing 提供的手段。
当然,如果你的单元测试代码仅仅是一个无须外部数据支持的计算函数,那你根本无须理会初始化或者清理,直接将单元测试代码编写为 xxx_test.go
中 TestXXX
函数即可。
- 初始化 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 方式
TestMain
是 go 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 部分。