go 子测试 简单的说,子测试提供一种在一个测试函数中执行多个测试的能力,比如原来有TestA、TestB和TestC三个测试函数,每个测试函数执行开始都需要做些相同的初始化工作,那么可以利用子测试将这三个测试合并到一个测试中,这样初始化工作只需要做一次。
除此之外,子测试还提供了诸多便利,下面我们逐一说明。
简单例子 我们先看一个简单的例子,以便快速了解子测试的基本用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package gotest_testimport ( "testing" "gotest" ) func sub1 (t *testing.T) { var a = 1 var b = 2 var expected = 3 actual := gotest.Add(a, b) if actual != expected { t.Errorf("Add(%d, %d) = %d; expected: %d" , a, b, actual, expected) } } func sub2 (t *testing.T) { var a = 1 var b = 2 var expected = 3 actual := gotest.Add(a, b) if actual != expected { t.Errorf("Add(%d, %d) = %d; expected: %d" , a, b, actual, expected) } } func sub3 (t *testing.T) { var a = 1 var b = 2 var expected = 3 actual := gotest.Add(a, b) if actual != expected { t.Errorf("Add(%d, %d) = %d; expected: %d" , a, b, actual, expected) } } func TestSub (t *testing.T) { t.Run("A=1" , sub1) t.Run("A=2" , sub2) t.Run("B=1" , sub3) }
本例中TestSub()
通过t.Run()
依次执行三个子测试。t.Run()
函数声明如下:
1 func (t *T) Run(name string , f func (t *T) ) bool
name
参数为子测试的名字,f
为子测试函数,本例中Run()
一直阻塞到f
执行结束后才返回,返回值为f的执行结果。Run()
会启动新的协程来执行f
,并阻塞等待f
执行结束才返回,除非f
中使用t.Parallel()
设置子测试为并发。
本例中TestSub()
把三个子测试合并起来,可以共享setup和tear-down部分的代码。
我们在命令行下,使用-v
参数执行测试:
1 2 3 4 5 6 7 8 9 10 11 E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subunit_test.go -v === RUN TestSub === RUN TestSub/A=1 === RUN TestSub/A=2 === RUN TestSub/B=1 --- PASS: TestSub (0.00s) --- PASS: TestSub/A=1 (0.00s) --- PASS: TestSub/A=2 (0.00s) --- PASS: TestSub/B=1 (0.00s) PASS ok command-line-arguments 0.354s
从输出中可以看出,三个子测试都被执行到了,而且执行次序与调用次序一致。
子测试命名规则 通过上面的例子我们知道Run()
方法第一个参数为子测试的名字,而实际上子测试的内部命名规则为:”*<父测试名字>/ <传递给Run的名字>*“。比如,传递给Run()
的名字是“A=1”,那么子测试名字为“TestSub/A=1”。这个在上面的命令行输出中也可以看出。
过滤筛选 通过测试的名字,可以在执行中过滤掉一部分测试。
比如,只执行上例中“A=*”的子测试,那么执行时使用-run Sub/A=
参数即可:
1 2 3 4 5 6 7 8 9 E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subunit_test.go -v -run Sub/A= === RUN TestSub === RUN TestSub/A=1 === RUN TestSub/A=2 --- PASS: TestSub (0.00s) --- PASS: TestSub/A=1 (0.00s) --- PASS: TestSub/A=2 (0.00s) PASS ok command-line-arguments 0.340s
上例中,使用参数-run Sub/A=
则只会执行TestSub/A=1
和TestSub/A=2
两个子测试。
对于子性能测试则使用-bench
参数来筛选,此处不再赘述。
注意:此处的筛选不是严格的正则匹配,而是包含匹配。比如,-run A=
那么所有测试(含子测试)的名字中如果包含“A=”则会被选中执行。
子测试并发 前面提到的多个子测试共享setup和teardown有一个前提是子测试没有并发,如果子测试使用t.Parallel()
指定并发,那么就没办法共享teardown了,因为执行顺序很可能是setup->子测试1->teardown->子测试2…。
如果子测试可能并发,则可以把子测试通过Run()
再嵌套一层,Run()
可以保证其下的所有子测试执行结束后再返回。
为便于说明,我们创建文件subparallel_test.go
用于说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package gotest_testimport ( "testing" "time" ) func parallelTest1 (t *testing.T) { t.Parallel() time.Sleep(3 * time.Second) } func parallelTest2 (t *testing.T) { t.Parallel() time.Sleep(2 * time.Second) } func parallelTest3 (t *testing.T) { t.Parallel() time.Sleep(1 * time.Second) } func TestSubParallel (t *testing.T) { t.Logf("Setup" ) t.Run("group" , func (t *testing.T) { t.Run("Test1" , parallelTest1) t.Run("Test2" , parallelTest2) t.Run("Test3" , parallelTest3) }) t.Logf("teardown" ) }
上面三个子测试中分别sleep了3s、2s、1s用于观察并发执行顺序。通过Run()
将多个子测试“封装”到一个组中,可以保证所有子测试全部执行结束后再执行tear-down。
命令行下的输出如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subparallel_test.go -v -run SubParallel === RUN TestSubParallel === RUN TestSubParallel/group === RUN TestSubParallel/group/Test1 === RUN TestSubParallel/group/Test2 === RUN TestSubParallel/group/Test3 --- PASS: TestSubParallel (3.01 s) subparallel_test.go :25 : Setup --- PASS: TestSubParallel/group (0.00 s) --- PASS: TestSubParallel/group/Test3 (1.00 s) --- PASS: TestSubParallel/group/Test2 (2.01 s) --- PASS: TestSubParallel/group/Test1 (3.01 s) subparallel_test.go :34 : teardown PASS ok command-line-arguments 3.353 s
通过该输出可以看出:
子测试是并发执行的(Test1最先被执行却最后结束)
tear-down在所有子测试结束后才执行
总结
子测试适用于单元测试和性能测试;
子测试可以控制并发;
子测试提供一种类似table-driven风格的测试;
子测试可以共享setup和tear-down;