简介

简单的说,子测试提供一种在一个测试函数中执行多个测试的能力,比如原来有TestA、TestB和TestC三个测试函数,每个测试函数执行开始都需要做些相同的初始化工作,那么可以利用子测试将这三个测试合并到一个测试中,这样初始化工作只需要做一次。

除此之外,子测试还提供了诸多便利,下面我们逐一说明。

简单例子

我们先看一个简单的例子,以便快速了解子测试的基本用法。

  1. package gotest_test
  2. import (
  3. "testing"
  4. "gotest"
  5. )
  6. // sub1 为子测试,只做加法测试
  7. func sub1(t *testing.T) {
  8. var a = 1
  9. var b = 2
  10. var expected = 3
  11. actual := gotest.Add(a, b)
  12. if actual != expected {
  13. t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)
  14. }
  15. }
  16. // sub2 为子测试,只做加法测试
  17. func sub2(t *testing.T) {
  18. var a = 1
  19. var b = 2
  20. var expected = 3
  21. actual := gotest.Add(a, b)
  22. if actual != expected {
  23. t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)
  24. }
  25. }
  26. // sub3 为子测试,只做加法测试
  27. func sub3(t *testing.T) {
  28. var a = 1
  29. var b = 2
  30. var expected = 3
  31. actual := gotest.Add(a, b)
  32. if actual != expected {
  33. t.Errorf("Add(%d, %d) = %d; expected: %d", a, b, actual, expected)
  34. }
  35. }
  36. // TestSub 内部调用sub1、sub2和sub3三个子测试
  37. func TestSub(t *testing.T) {
  38. // setup code
  39. t.Run("A=1", sub1)
  40. t.Run("A=2", sub2)
  41. t.Run("B=1", sub3)
  42. // tear-down code
  43. }

本例中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. E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subunit_test.go -v
  2. === RUN TestSub
  3. === RUN TestSub/A=1
  4. === RUN TestSub/A=2
  5. === RUN TestSub/B=1
  6. --- PASS: TestSub (0.00s)
  7. --- PASS: TestSub/A=1 (0.00s)
  8. --- PASS: TestSub/A=2 (0.00s)
  9. --- PASS: TestSub/B=1 (0.00s)
  10. PASS
  11. ok command-line-arguments 0.354s

从输出中可以看出,三个子测试都被执行到了,而且执行次序与调用次序一致。

子测试命名规则

通过上面的例子我们知道Run()方法第一个参数为子测试的名字,而实际上子测试的内部命名规则为:”<父测试名字>/<传递给Run的名字>“。比如,传递给Run()的名字是“A=1”,那么子测试名字为“TestSub/A=1”。这个在上面的命令行输出中也可以看出。

过滤筛选

通过测试的名字,可以在执行中过滤掉一部分测试。

比如,只执行上例中“A=*”的子测试,那么执行时使用-run Sub/A=参数即可:

  1. E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subunit_test.go -v -run Sub/A=
  2. === RUN TestSub
  3. === RUN TestSub/A=1
  4. === RUN TestSub/A=2
  5. --- PASS: TestSub (0.00s)
  6. --- PASS: TestSub/A=1 (0.00s)
  7. --- PASS: TestSub/A=2 (0.00s)
  8. PASS
  9. ok command-line-arguments 0.340s

上例中,使用参数-run Sub/A=则只会执行TestSub/A=1TestSub/A=2两个子测试。

对于子性能测试则使用-bench参数来筛选,此处不再赘述。

注意:此处的筛选不是严格的正则匹配,而是包含匹配。比如,-run A=那么所有测试(含子测试)的名字中如果包含“A=”则会被选中执行。

子测试并发

前面提到的多个子测试共享setup和teardown有一个前提是子测试没有并发,如果子测试使用t.Parallel()指定并发,那么就没办法共享teardown了,因为执行顺序很可能是setup->子测试1->teardown->子测试2…。

如果子测试可能并发,则可以把子测试通过Run()再嵌套一层,Run()可以保证其下的所有子测试执行结束后再返回。

为便于说明,我们创建文件subparallel_test.go用于说明:

  1. package gotest_test
  2. import (
  3. "testing"
  4. "time"
  5. )
  6. // 并发子测试,无实际测试工作,仅用于演示
  7. func parallelTest1(t *testing.T) {
  8. t.Parallel()
  9. time.Sleep(3 * time.Second)
  10. // do some testing
  11. }
  12. // 并发子测试,无实际测试工作,仅用于演示
  13. func parallelTest2(t *testing.T) {
  14. t.Parallel()
  15. time.Sleep(2 * time.Second)
  16. // do some testing
  17. }
  18. // 并发子测试,无实际测试工作,仅用于演示
  19. func parallelTest3(t *testing.T) {
  20. t.Parallel()
  21. time.Sleep(1 * time.Second)
  22. // do some testing
  23. }
  24. // TestSubParallel 通过把多个子测试放到一个组中并发执行,同时多个子测试可以共享setup和tear-down
  25. func TestSubParallel(t *testing.T) {
  26. // setup
  27. t.Logf("Setup")
  28. t.Run("group", func(t *testing.T) {
  29. t.Run("Test1", parallelTest1)
  30. t.Run("Test2", parallelTest2)
  31. t.Run("Test3", parallelTest3)
  32. })
  33. // tear down
  34. t.Logf("teardown")
  35. }

上面三个子测试中分别sleep了3s、2s、1s用于观察并发执行顺序。通过Run()将多个子测试“封装”到一个组中,可以保证所有子测试全部执行结束后再执行tear-down。

命令行下的输出如下:

  1. E:\OpenSource\GitHub\RainbowMango\GoExpertProgrammingSourceCode\GoExpert\src\gotest>go test subparallel_test.go -v -run SubParallel
  2. === RUN TestSubParallel
  3. === RUN TestSubParallel/group
  4. === RUN TestSubParallel/group/Test1
  5. === RUN TestSubParallel/group/Test2
  6. === RUN TestSubParallel/group/Test3
  7. --- PASS: TestSubParallel (3.01s)
  8. subparallel_test.go:25: Setup
  9. --- PASS: TestSubParallel/group (0.00s)
  10. --- PASS: TestSubParallel/group/Test3 (1.00s)
  11. --- PASS: TestSubParallel/group/Test2 (2.01s)
  12. --- PASS: TestSubParallel/group/Test1 (3.01s)
  13. subparallel_test.go:34: teardown
  14. PASS
  15. ok command-line-arguments 3.353s

通过该输出可以看出:

  1. 子测试是并发执行的(Test1最先被执行却最后结束)
  2. tear-down在所有子测试结束后才执行

总结

  • 子测试适用于单元测试和性能测试;
  • 子测试可以控制并发;
  • 子测试提供一种类似table-driven风格的测试;
  • 子测试可以共享setup和tear-down;