testing - 单元测试

testing 为 Go 语言 package 提供自动化测试的支持。通过 go test 命令,能够自动执行如下形式的任何函数:

  1. func TestXxx(*testing.T)

注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。

在这些函数中,使用 ErrorFail 或相关方法来发出失败信号。

要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx 函数,如上所述。 将该文件放在与被测试文件相同的包中。该文件将被排除在正常的程序包之外,但在运行 go test 命令时将被包含。 有关详细信息,请运行 go help testgo help testflag 了解。

如果有需要,可以调用 *T*BSkip 方法,跳过该测试或基准测试:

  1. func TestTimeConsuming(t *testing.T) {
  2. if testing.Short() {
  3. t.Skip("skipping test in short mode.")
  4. }
  5. ...
  6. }

第一个单元测试

要测试的代码:

  1. func Fib(n int) int {
  2. if n < 2 {
  3. return n
  4. }
  5. return Fib(n-1) + Fib(n-2)
  6. }

测试代码:

  1. func TestFib(t *testing.T) {
  2. var (
  3. in = 7
  4. expected = 13
  5. )
  6. actual := Fib(in)
  7. if actual != expected {
  8. t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
  9. }
  10. }

执行 go test .,输出:

  1. $ go test .
  2. ok chapter09/testing 0.007s

表示测试通过。

我们将 Sum 函数改为:

  1. func Fib(n int) int {
  2. if n < 2 {
  3. return n
  4. }
  5. return Fib(n-1) + Fib(n-1)
  6. }

再执行 go test .,输出:

  1. $ go test .
  2. --- FAIL: TestSum (0.00s)
  3. t_test.go:16: Fib(10) = 64; expected 13
  4. FAIL
  5. FAIL chapter09/testing 0.009s

Table-Driven Test

测试讲究 case 覆盖,按上面的方式,当我们要覆盖更多 case 时,显然通过修改代码的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。

  1. func TestFib(t *testing.T) {
  2. var fibTests = []struct {
  3. in int // input
  4. expected int // expected result
  5. }{
  6. {1, 1},
  7. {2, 1},
  8. {3, 2},
  9. {4, 3},
  10. {5, 5},
  11. {6, 8},
  12. {7, 13},
  13. }
  14. for _, tt := range fibTests {
  15. actual := Fib(tt.in)
  16. if actual != tt.expected {
  17. t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
  18. }
  19. }
  20. }

由于我们使用的是 t.Errorf,即使其中某个 case 失败,也不会终止测试执行。

T 类型

单元测试中,传递给测试函数的参数是 *testing.T 类型。它用于管理测试状态并支持格式化测试日志。测试日志会在执行测试的过程中不断累积,并在测试完成时转储至标准输出。

当测试函数返回时,或者当测试函数调用 FailNowFatalFatalfSkipNowSkipSkipf 中的任意一个时,则宣告该测试函数结束。跟 Parallel 方法一样,以上提到的这些方法只能在运行测试函数的 goroutine 中调用。

至于其他报告方法,比如 Log 以及 Error 的变种, 则可以在多个 goroutine 中同时进行调用。

报告方法

上面提到的系列包括方法,带 f 的是格式化的,格式化语法参考 fmt 包。

T 类型内嵌了 common 类型,common 提供这一系列方法,我们经常会用到的(注意,这里说的测试中断,都是指当前测试函数):

1)当我们遇到一个断言错误的时候,标识这个测试失败,会使用到:

  1. Fail : 测试失败,测试继续,也就是之后的代码依然会执行
  2. FailNow : 测试失败,测试中断

FailNow 方法实现的内部,是通过调用 runtime.Goexit() 来中断测试的。

2)当我们遇到一个断言错误,只希望跳过这个错误,但是不希望标识测试失败,会使用到:

  1. SkipNow : 跳过测试,测试中断

SkipNow 方法实现的内部,是通过调用 runtime.Goexit() 来中断测试的。

3)当我们只希望打印信息,会用到 :

  1. Log : 输出信息
  2. Logf : 输出格式化的信息

注意:默认情况下,单元测试成功时,它们打印的信息不会输出,可以通过加上 -v 选项,输出这些信息。但对于基准测试,它们总是会被输出。

4)当我们希望跳过这个测试,并且打印出信息,会用到:

  1. Skip : 相当于 Log + SkipNow
  2. Skipf : 相当于 Logf + SkipNow

5)当我们希望断言失败的时候,标识测试失败,并打印出必要的信息,但是测试继续,会用到:

  1. Error : 相当于 Log + Fail
  2. Errorf : 相当于 Logf + Fail

6)当我们希望断言失败的时候,标识测试失败,打印出必要的信息,但中断测试,会用到:

  1. Fatal : 相当于 Log + FailNow
  2. Fatalf : 相当于 Logf + FailNow

Parallel 测试

包中的 Parallel 方法表示当前测试只会与其他带有 Parallel 方法的测试并行进行测试。

下面例子将演示 Parallel 的使用方法:

  1. var (
  2. data = make(map[string]string)
  3. locker sync.RWMutex
  4. )
  5. func WriteToMap(k, v string) {
  6. locker.Lock()
  7. defer locker.Unlock()
  8. data[k] = v
  9. }
  10. func ReadFromMap(k string) string {
  11. locker.RLock()
  12. defer locker.RUnlock()
  13. return data[k]
  14. }

测试代码:

  1. var pairs = []struct {
  2. k string
  3. v string
  4. }{
  5. {"polaris", " 徐新华 "},
  6. {"studygolang", "Go 语言中文网 "},
  7. {"stdlib", "Go 语言标准库 "},
  8. {"polaris1", " 徐新华 1"},
  9. {"studygolang1", "Go 语言中文网 1"},
  10. {"stdlib1", "Go 语言标准库 1"},
  11. {"polaris2", " 徐新华 2"},
  12. {"studygolang2", "Go 语言中文网 2"},
  13. {"stdlib2", "Go 语言标准库 2"},
  14. {"polaris3", " 徐新华 3"},
  15. {"studygolang3", "Go 语言中文网 3"},
  16. {"stdlib3", "Go 语言标准库 3"},
  17. {"polaris4", " 徐新华 4"},
  18. {"studygolang4", "Go 语言中文网 4"},
  19. {"stdlib4", "Go 语言标准库 4"},
  20. }
  21. // 注意 TestWriteToMap 需要在 TestReadFromMap 之前
  22. func TestWriteToMap(t *testing.T) {
  23. t.Parallel()
  24. for _, tt := range pairs {
  25. WriteToMap(tt.k, tt.v)
  26. }
  27. }
  28. func TestReadFromMap(t *testing.T) {
  29. t.Parallel()
  30. for _, tt := range pairs {
  31. actual := ReadFromMap(tt.k)
  32. if actual != tt.v {
  33. t.Errorf("the value of key(%s) is %s, expected: %s", tt.k, actual, tt.v)
  34. }
  35. }
  36. }

试验步骤:

  1. 注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,同时注释掉测试代码中的 t.Parallel,执行测试,测试通过,即使加上 -race,测试依然通过;
  2. 只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,执行测试,测试失败(如果未失败,加上 -race 一定会失败);

如果代码能够进行并行测试,在写测试时,尽量加上 Parallel,这样可以测试出一些可能的问题。

关于 Parallel 的更多内容,会在 子测试 中介绍。

当你写完一个函数,结构体,main 之后,你下一步需要的就是测试了。testing 包提供了很简单易用的测试包。

写一个基本的测试用例

测试文件的文件名需要以_test.go 为结尾,测试用例需要以 TestXxxx 的形式存在。

比如我要测试 utils 包的 sql.go 中的函数:

  1. func GetOne(db *sql.DB, query string, args ...interface{}) (map[string][]byte, error) {

就需要创建一个 sql_test.go

  1. package utils
  2. import (
  3. "database/sql"
  4. _ "fmt"
  5. _ "github.com/go-sql-driver/mysql"
  6. "strconv"
  7. "testing"
  8. )
  9. func Test_GetOne(t *testing.T) {
  10. db, err := sql.Open("mysql", "root:123.abc@tcp(192.168.33.10:3306)/test")
  11. defer func() {
  12. db.Close()
  13. }()
  14. if err != nil {
  15. t.Fatal(err)
  16. }
  17. // 测试 empty
  18. car_brand, err := GetOne(db, "select * from user where id = 999999")
  19. if (car_brand != nil) || (err != nil) {
  20. t.Fatal("emtpy 测试错误 ")
  21. }
  22. }

testing 的测试用例形式

测试用例有四种形式:

  1. TestXxxx(t *testing.T) // 基本测试用例
  2. BenchmarkXxxx(b *testing.B) // 压力测试的测试用例
  3. Example_Xxx() // 测试控制台输出的例子
  4. TestMain(m *testing.M) // 测试 Main 函数

给个 Example 的例子 :(Example 需要在最后用注释的方式确认控制台输出和预期是不是一致的)

  1. func Example_GetScore() {
  2. score := getScore(100, 100, 100, 2.1)
  3. fmt.Println(score)
  4. // Output:
  5. // 31.1
  6. }

testing 的变量

gotest 的变量有这些:

  • test.short : 一个快速测试的标记,在测试用例中可以使用 testing.Short() 来绕开一些测试
  • test.outputdir : 输出目录
  • test.coverprofile : 测试覆盖率参数,指定输出文件
  • test.run : 指定正则来运行某个 / 某些测试用例
  • test.memprofile : 内存分析参数,指定输出文件
  • test.memprofilerate : 内存分析参数,内存分析的抽样率
  • test.cpuprofile : cpu 分析输出参数,为空则不做 cpu 分析
  • test.blockprofile : 阻塞事件的分析参数,指定输出文件
  • test.blockprofilerate : 阻塞事件的分析参数,指定抽样频率
  • test.timeout : 超时时间
  • test.cpu : 指定 cpu 数量
  • test.parallel : 指定运行测试用例的并行数

testing 的结构体

  • B : 压力测试
  • BenchmarkResult : 压力测试结果
  • Cover : 代码覆盖率相关结构体
  • CoverBlock : 代码覆盖率相关结构体
  • InternalBenchmark : 内部使用的结构体
  • InternalExample : 内部使用的结构体
  • InternalTest : 内部使用的结构体
  • M : main 测试使用的结构体
  • PB : Parallel benchmarks 并行测试使用的结构体
  • T : 普通测试用例
  • TB : 测试用例的接口

testing 的通用方法

T 结构内部是继承自 common 结构,common 结构提供集中方法,是我们经常会用到的:

1)当我们遇到一个断言错误的时候,我们就会判断这个测试用例失败,就会使用到:

  1. Fail : case 失败,测试用例继续
  2. FailedNow : case 失败,测试用例中断

2)当我们遇到一个断言错误,只希望跳过这个错误,但是不希望标示测试用例失败,会使用到:

  1. SkipNow : case 跳过,测试用例不继续

3)当我们只希望在一个地方打印出信息,我们会用到 :

  1. Log : 输出信息
  2. Logf : 输出有 format 的信息

4)当我们希望跳过这个用例,并且打印出信息 :

  1. Skip : Log + SkipNow
  2. Skipf : Logf + SkipNow

5)当我们希望断言失败的时候,测试用例失败,打印出必要的信息,但是测试用例继续:

  1. Error : Log + Fail
  2. Errorf : Logf + Fail

6)当我们希望断言失败的时候,测试用例失败,打印出必要的信息,测试用例中断:

  1. Fatal : Log + FailNow
  2. Fatalf : Logf + FailNow

扩展阅读

GO 中如何进行单元测试 GoDoc - testing testing/testing.go 源代码

导航