golang testing简介

时间:2025-02-26 20:05:37

单元测试

Golang中的单元测试对文件名、方法名和参数都有很严格的要求:

  1. 文件名必须以 xx_test.go 命名
  2. 方法必须是 Test[^a-z] 开头
  3. 方法参数必须 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 感兴趣的可以阅读源码,了解其原理。