🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
# testing - 单元测试 # `testing` 为 Go 语言 package 提供自动化测试的支持。通过 `go test` 命令,能够自动执行如下形式的任何函数: func TestXxx(*testing.T) 注意:Xxx 可以是任何字母数字字符串,但是第一个字母不能是小些字母。 在这些函数中,使用 Error, Fail 或相关方法来发出失败信号。 要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 `TestXxx` 函数,如上所述。 将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “go test” 命令时将被包含。 有关详细信息,请运行 “go help test” 和 “go help testflag” 了解。 如果有需要,可以调用 `*T` 和 `*B` 的 Skip 方法,跳过该测试或基准测试: ```go func TestTimeConsuming(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } ... } ``` ## 第一个单元测试 ## 要测试的代码: ```go func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-2) } ``` 测试代码: ```go func TestFib(t *testing.T) { var ( in = 7 expected = 13 ) actual := Fib(in) if actual != expected { t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected) } } ``` 执行 `go test .`,输出: ``` $ go test . ok chapter09/testing 0.007s ``` 表示测试通过。 我们将 `Sum` 函数改为: ```go func Fib(n int) int { if n < 2 { return n } return Fib(n-1) + Fib(n-1) } ``` 再执行 `go test .`,输出: ``` $ go test . --- FAIL: TestSum (0.00s) t_test.go:16: Fib(10) = 64; expected 13 FAIL FAIL chapter09/testing 0.009s ``` ## Table-Driven Test ## 测试讲究 case 覆盖,按上面的方式,当我们要覆盖更多 case 时,显然通过修改代码的方式很笨拙。这时我们可以采用 Table-Driven 的方式写测试,标准库中有很多测试是使用这种方式写的。 ```go func TestFib(t *testing.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(tt.in) if actual != tt.expected { t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected) } } } ``` 因为我们使用的是 `t.Errorf`,其中某个 case 失败,并不会终止测试执行。 ## T 类型 ## 单元测试中,传递给测试函数的参数是 `*testing.T` 类型。它用于管理测试状态并支持格式化测试日志。测试日志会在执行测试的过程中不断累积,并在测试完成时转储至标准输出。 当一个测试的测试函数返回时,又或者当一个测试函数调用 `FailNow`、 `Fatal`、`Fatalf`、`SkipNow`、`Skip` 或者 `Skipf` 中的任意一个时,该测试即宣告结束。跟 `Parallel` 方法一样,以上提到的这些方法只能在运行测试函数的 goroutine 中调用。 至于其他报告方法,比如 `Log` 以及 `Error` 的变种, 则可以在多个 goroutine 中同时进行调用。 ### 报告方法 ### 上面提到的系列包括方法,带 `f` 的是格式化的,格式化语法参考 `fmt` 包。 T 类型内嵌了 common 类型,common 提供这一系列方法,我们经常会用到的(注意,这里说的测试中断,都是指当前测试函数): 1)当我们遇到一个断言错误的时候,标识这个测试失败,会使用到: Fail: 测试失败,测试继续,也就是之后的代码依然会执行 FailNow: 测试失败,测试中断 在 `FailNow ` 方法实现的内部,是通过调用 `runtime.Goexit()` 来中断测试的。 2)当我们遇到一个断言错误,只希望跳过这个错误,但是不希望标识测试失败,会使用到: SkipNow: 跳过测试,测试中断 在 `SkipNow` 方法实现的内部,是通过调用 `runtime.Goexit()` 来中断测试的。 3)当我们只希望打印信息,会用到: Log: 输出信息 Logf: 输出格式化的信息 注意:默认情况下,单元测试成功时,它们打印的信息不会输出,可以通过加上 `-v` 选项,输出这些信息。但对于基准测试,它们总是会被输出。 4)当我们希望跳过这个测试,并且打印出信息,会用到: Skip: 相当于 Log + SkipNow Skipf: 相当于 Logf + SkipNow 5)当我们希望断言失败的时候,标识测试失败,并打印出必要的信息,但是测试继续,会用到: Error: 相当于 Log + Fail Errorf: 相当于 Logf + Fail 6)当我们希望断言失败的时候,标识测试失败,打印出必要的信息,但中断测试,会用到 Fatal: 相当于 Log + FailNow Fatalf: 相当于 Logf + FailNow ### Parallel 测试 ### 包中的 Parallel 方法用于表示当前测试只会与其他带有 Parallel 方法的测试并行进行测试。 下面的例子演示的 Parallel 的使用: ```go var ( data = make(map[string]string) locker sync.RWMutex ) func WriteToMap(k, v string) { locker.Lock() defer locker.Unlock() data[k] = v } func ReadFromMap(k string) string { locker.RLock() defer locker.RUnlock() return data[k] } ``` 测试代码: ```go var pairs = []struct { k string v string }{ {"polaris", "徐新华"}, {"studygolang", "Go语言中文网"}, {"stdlib", "Go语言标准库"}, {"polaris1", "徐新华1"}, {"studygolang1", "Go语言中文网1"}, {"stdlib1", "Go语言标准库1"}, {"polaris2", "徐新华2"}, {"studygolang2", "Go语言中文网2"}, {"stdlib2", "Go语言标准库2"}, {"polaris3", "徐新华3"}, {"studygolang3", "Go语言中文网3"}, {"stdlib3", "Go语言标准库3"}, {"polaris4", "徐新华4"}, {"studygolang4", "Go语言中文网4"}, {"stdlib4", "Go语言标准库4"}, } // 注意 TestWriteToMap 需要在 TestReadFromMap 之前 func TestWriteToMap(t *testing.T) { t.Parallel() for _, tt := range pairs { WriteToMap(tt.k, tt.v) } } func TestReadFromMap(t *testing.T) { t.Parallel() for _, tt := range pairs { actual := ReadFromMap(tt.k) if actual != tt.v { t.Errorf("the value of key(%s) is %s, expected: %s", tt.k, actual, tt.v) } } } ``` 试验步骤: 1. 注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,同时注释掉测试代码中的 t.Parallel,执行测试,测试通过,即使加上 `-race`,测试依然通过; 2. 只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,执行测试,测试失败(如果未失败,加上 `-race` 一定会失败); 如果代码能够进行并行测试,在写测试时,尽量加上 Parallel,这样可以测试出一些可能的问题。 关于 Parallel 的更多内容,会在 [子测试](chapter09/09.3.md) 中介绍。 当你写完一个函数,结构体,main之后,你下一步需要的就是测试了。testing包提供了很简单易用的测试包。 # 写一个基本的测试用例 # 测试文件的文件名需要以_test.go为结尾,测试用例需要以TestXxxx的样式存在。 比如我要测试utils包的sql.go中的函数: func GetOne(db *sql.DB, query string, args ...interface{}) (map[string][]byte, error) { 就需要创建一个sql_test.go package utils import ( "database/sql" _ "fmt" _ "github.com/go-sql-driver/mysql" "strconv" "testing" ) func Test_GetOne(t *testing.T) { db, err := sql.Open("mysql", "root:123.abc@tcp(192.168.33.10:3306)/test") defer func() { db.Close() }() if err != nil { t.Fatal(err) } // 测试empty car_brand, err := GetOne(db, "select * from user where id = 999999") if (car_brand != nil) || (err != nil) { t.Fatal("emtpy测试错误") } } #testing的测试用例形式# 测试用例有四种形式: TestXxxx(t *testing.T) // 基本测试用例 BenchmarkXxxx(b *testing.B) // 压力测试的测试用例 Example_Xxx() // 测试控制台输出的例子 TestMain(m *testing.M) // 测试Main函数 给个Example的例子:(Example需要在最后用注释的方式确认控制台输出和预期是不是一致的) func Example_GetScore() { score := getScore(100, 100, 100, 2.1) fmt.Println(score) // Output: // 31.1 } #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结构提供集中方法,是我们经常会用到的: 当我们遇到一个断言错误的时候,我们就会判断这个测试用例失败,就会使用到: 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 ## 扩展阅读 [GO 中如何进行单元测试](http://blog.studygolang.com/2017/10/how-to-test-with-go/) # 导航 # - [第九章](/chapter09/09.0.md) - 下一节:[testing - 基准测试](09.2.md)