单元测试
Golang中的单元测试对文件名、方法名和参数都有很严格的要求:
- 文件名必须以 xx_test.go 命名
- 方法必须是 Test[^a-z] 开头
- 方法参数必须 t *
testing的通用方法
T结构内部是继承自common结构,common结构提供集中方法,是我们经常会用到的:
当我们遇到一个断言错误的时候,我们就会判断这个测试用例失败,就会使用到:
Fail : case失败,测试用例继续
FailedNow : case失败,测试用例中断
当我们遇到一个断言错误,只希望跳过这个错误,但是不希望标示测试用例失败,会使用到:
SkipNow : case跳过,测试用例不继续
当我们只希望在一个地方打印出信息,我们会用到:
Log : 输出信息
Logf : 输出有format的信息
当我们希望跳过这个用例,并且打印出信息:
Skip : Log + SkipNow
Skipf : Logf + SkipNow
当我们希望断言失败的时候,测试用例失败,打印出必要的信息,但是测试用例继续:
Error : Log + Fail
Errorf : Logf + Fail
当我们希望断言失败的时候,测试用例失败,打印出必要的信息,测试用例中断:
Fatal : Log + FailNow
Fatalf : Logf + FailNow
表驱动测试
测试讲究 case 覆盖,当我们要覆盖更多 case 时,显然通过修改代码一一测试的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。
func TestFib(t *) {
var fibTests = []struct {
in int // input
expected int // expected result
}{
{1, 1},
{2, 1},
{3, 2},
{4, 3},
{5, 5},
{6, 8},
{7, 13},
}
for _, tt := range fibTests {
actual := Fib()
if actual != {
("Fib(%d) = %d; expected %d", , actual, )
}
}
}
基准测试
在 _test.go 结尾的测试文件中,如下形式的函数:
func BenchmarkXxx(*)
被认为是基准测试,通过 "go test" 命令,加上 -bench
flag 来执行。多个基准测试按照顺序运行。
基准测试函数样例看起来如下所示:
func BenchmarkHello(b *) {
for i := 0; i < ; i++ {
("hello")
}
}
基准函数会运行目标代码 次。在基准执行期间,会调整 直到基准测试函数运行时间稳定。输出
BenchmarkHello 10000000 282 ns/op
意味着循环执行了 10000000 次,每次循环花费 282 纳秒(ns)。
如果在运行前基准测试需要一些耗时的配置,则可以先重置定时器:
func BenchmarkBigLen(b *) {
big := NewBig()
()
for i := 0; i < ; i++ {
()
}
}
子测试
从 Go 1.7 开始,引入了一个新特性:子测试,又叫 命名测试(named tests),它意味着您现在可以拥有嵌套测试,这对于自定义(和过滤)给定测试的示例非常有用。
T 和 B 的 Run 方法允许定义子单元测试和子基准测试,而不必为每个子测试和子基准定义单独的函数。这使得可以使用 Table-Driven 的基准测试和创建层级测试。它还提供了一种共享通用 setup
和 tear-down
代码的方法:
func TestFoo(t *) {
// <setup code>
("A=1", func(t *) { ... })
("A=2", func(t *) { ... })
("B=1", func(t *) { ... })
// <tear-down code>
}
如果不想用匿名函数,可以将子测试函数名首字母小写,如:
// 父测试流程
func TestUserWorkFlow(t *) {
("Add", testAddUser)
("Get", testGetUser)
("Del", testDeleteUser)
("Reget", testRegetUser)
}
// 子测试
func testAddUser(t *) {
err := AddUserCredential("avenssi", "123")
if err != nil {
("Error of AddUser: %v", err)
}
}
func testGetUser(t *) {
pwd, err := GetUserCredential("avenssi")
if pwd != "123" || err != nil {
("Error of GetUser")
}
}
func testDeleteUser(t *) {
err := DeleteUser("avenssi", "123")
if err != nil {
("Error of DeleteUser: %v", err)
}
}
func testRegetUser(t *) {
pwd, err := GetUserCredential("avenssi")
if err != nil {
("Error of RegetUser: %v", err)
}
if pwd != "" {
("Deleting user test failed")
}
}
每个子测试和子基准测试都有一个唯一的名称:*测试的名称和传递给 Run 的名称的组合,以斜杠分隔,并具有用于消歧的可选尾随序列号。
-run
和 -bench
命令行标志的参数是与测试名称相匹配的非固定的正则表达式。对于具有多个斜杠分隔元素(例如子测试)的测试,该参数本身是斜杠分隔的,其中表达式依次匹配每个名称元素。因为它是非固定的,一个空的表达式匹配任何字符串。例如,使用 "匹配" 表示 "其名称包含":
go test -run '' # Run 所有测试。
go test -run Foo # Run 匹配 "Foo" 的顶层测试,例如 "TestFooBar"。
go test -run Foo/A= # 匹配顶层测试 "Foo",运行其匹配 "A=" 的子测试。
go test -run /A=1 # 运行所有匹配 "A=1" 的子测试。
TestMain
在写测试时,有时需要在测试之前或之后进行额外的设置(setup)或拆卸(teardown);有时,测试还需要控制在主线程上运行的代码。为了支持这些需求,testing 提供了 TestMain 函数:
func TestMain(m *)
如果测试文件中包含该函数,那么生成的测试将调用 TestMain(m),而不是直接运行测试。TestMain 运行在主 goroutine 中, 可以在调用 前后做任何设置和拆卸。注意,在 TestMain 函数的最后,应该使用 的返回值作为参数调用 。
另外,在调用 TestMain 时, 并没有被调用。所以,如果 TestMain 依赖于 command-line 标志 (包括 testing 包的标记), 则应该显示的调用 。注意,这里说的依赖,说的是如果 TestMain 函数内依赖 flag,则必须显示调用 ,否则不需要,因为 中调用 。
一个包含 TestMain 的例子如下:
package mytestmain
import (
"flag"
"fmt"
"os"
"testing"
)
var db struct {
Dns string
}
func TestMain(m *) {
= ("DATABASE_DNS")
if == "" {
= "root:123456@tcp(localhost:3306)/?charset=utf8&parseTime=True&loc=Local"
}
()
exitCode := ()
= ""
// 退出
(exitCode)
}
func TestDatabase(t *) {
()
}
对 Run
感兴趣的可以阅读源码,了解其原理。