简介

在了解过testing.common后,我们进一步了解testing.T数据结构,以便了解更多单元测试执行的更多细节。

数据结构

源码包src/testing/testing.go:T定义了其数据结构:

  1. type T struct {
  2. common
  3. isParallel bool
  4. context *testContext // For running tests and subtests.
  5. }

其成员简单介绍如下:

  • common: 即前面绍的testing.common
  • isParallel: 表示当前测试是否需要并发,如果测试中执行了t.Parallel(),则此值为true
  • context: 控制测试的并发调度

因为context直接决定了单元测试的调度,在介绍testing.T支持的方法前,有必要先了解一下context。

testContext

源码包src/testing/testing.go:testContext定义了其数据结构:

  1. type testContext struct {
  2. match *matcher
  3. mu sync.Mutex
  4. // Channel used to signal tests that are ready to be run in parallel.
  5. startParallel chan bool
  6. // running is the number of tests currently running in parallel.
  7. // This does not include tests that are waiting for subtests to complete.
  8. running int
  9. // numWaiting is the number tests waiting to be run in parallel.
  10. numWaiting int
  11. // maxParallel is a copy of the parallel flag.
  12. maxParallel int
  13. }

testContext成员简单介绍如下:

  • match:匹配器,用于管理测试名称匹配、过滤等。
  • mu:互斥锁,用于控制testContext成员的互斥访问;
  • startParallel: 用于通知测试可以并发执行的控制管道,测试并发达到最大限制时,需要阻塞等待该管道的通知事件;
  • running: 当前并发执行的测试个数;
  • numWaiting:等待并发执行的测试个数,所有等待执行的测试都阻塞在startParallel管道处;
  • maxParallel:最大并发数,默认为系统CPU数,可以通过参数-parallel n指定。

testContext实现了两个方法用于控制测试发调度。

等待并发执行:testContext.waitParallel()

如果一个测试使用t.Parallel()启动并发,这个测试并不是立即被并发执行,需要检查当前并发执行的测试数量是否达到最大值,这个检查工作统一放在testContext.waitParallel()实现的。

testContext.waitParallel()函数的源码如下:

  1. func (c *testContext) waitParallel() {
  2. c.mu.Lock()
  3. if c.running < c.maxParallel { // 如果当前运行的测试数未达到最大值,直接返回
  4. c.running++
  5. c.mu.Unlock()
  6. return
  7. }
  8. c.numWaiting++ // 如果当前运行的测试数已达最大值,需要阻塞等待
  9. c.mu.Unlock()
  10. <-c.startParallel
  11. }

函数实现比较简单,如果当前运行的测试数未达最大值,将c.running++后直接返回即可,否则将c.numWaiting++并阻塞等待其他并发测试结束。

这里有个小细节,阻塞等待后面并没有累加c.running,因为其他并发的测试结束后也不会递减c.running,所以这里阻塞返回时也不用累加,一个测试结束,随即另一个测试开始,c.running个数没有变化。

并发测试结束:testContext.release()

当并发测试结束后,会通过release()方法释放一个信号,用于启动其他等待并发测试的函数。

testContext.release()函数的源码如下:

  1. func (c *testContext) release() {
  2. c.mu.Lock()
  3. if c.numWaiting == 0 { // 如果没有函数在等待,直接返回
  4. c.running--
  5. c.mu.Unlock()
  6. return
  7. }
  8. c.numWaiting-- // 如果有函数在等待,释放一个信号
  9. c.mu.Unlock()
  10. c.startParallel <- true // Pick a waiting test to be run.
  11. }

测试执行:tRunner()

函数tRunner用于执行一个测试,在不考虑并发测试、子测试场景下,其处理逻辑如下:

  1. func tRunner(t *T, fn func(t *T)) {
  2. defer func() {
  3. t.duration += time.Since(t.start)
  4. signal := true
  5. t.report() // 测试执行结束后向父测试报告日志
  6. t.done = true
  7. t.signal <- signal // 向调度者发送结束信号
  8. }()
  9. t.start = time.Now()
  10. fn(t)
  11. t.finished = true
  12. }

tRunner传一个经调度者设置过的testing.T参数和一个测试函数,执行时记录开始时间,然后将testing.T参数传入测试函数并同步等待其结束。

tRunner在defer语句中记录测试执行耗时,并上报日志,最后发送结束信号。

为了避免困惑,上述代码屏蔽了一些子测试和并发测试的细节,比如,defer语句中,如果当前测试包含子测试,则需要等所有子测试结束,如果当前测试为并发测试,则需要唤醒其他等待并发的测试。更多细节,等我们分析Parallel()和Run()时再讨论。

启动子测试:Run()

Run()函数用于启动一个子测试,这个子测试可以是用户的测试函数中主动调用Run()方法启动的,如果用户没有主动调用Run()方法,那么用户的测试函数也是被调度程序以Run()方法启动的。可以说,所有的测试都是Run()方法启动的。

按照惯例,隐去部分代码后的Run()方法如下所示:

  1. func (t *T) Run(name string, f func(t *T)) bool {
  2. t = &T{ // 创建一个新的testing.T用于执行子测试
  3. common: common{
  4. barrier: make(chan bool),
  5. signal: make(chan bool),
  6. name: testName,
  7. parent: &t.common,
  8. level: t.level + 1, // 子测试层次+1
  9. chatty: t.chatty,
  10. },
  11. context: t.context, // 子测试的context与父测试相同
  12. }
  13. go tRunner(t, f) // 启动协程执行子测试
  14. if !<-t.signal { // 阻塞等待子测试结束信号,子测试要么执行结束,要么以Parallel()执行。如果信号为'false',说明出现异常退出
  15. runtime.Goexit()
  16. }
  17. return !t.failed // 返回子测试的执行结果
  18. }

每启动一个子测试都会创建一个testing.T变量,该变量继承当前测试的部分属性,然后以新协程去执行,当前测试会在子测试结束后返回子测试的结果。

子测试退出条件要么是子测试执行结束,要么是子测试设置了Paraller(),否则是异常退出。

启动并发测试:Parallel()

Parallel()方法将当前测试加入到并发队列中,其实现方法如下所示:

  1. func (t *T) Parallel() {
  2. t.isParallel = true
  3. t.duration += time.Since(t.start) // 启动并发测试有可能要等待,等待期间耗时需要剔除,此处相当于先记录当前耗时,并发执行开始后再累加
  4. t.parent.sub = append(t.parent.sub, t) // 将当前测试加入到父测试的列表中,由父测试调度
  5. t.signal <- true // Release calling test. 当前测试即将进入并发模式,标记测试结束,以便父测试不必等待并退出Run()
  6. <-t.parent.barrier // Wait for the parent test to complete. 等待父测试发送子测试启动信号
  7. t.context.waitParallel() // 阻塞等待并发调度
  8. t.start = time.Now() // 开始并发执行,重新标记启动时间,这是第二段耗时
  9. }

关于测试耗时统计,看过前面的testContext实现我们知道,启动一个并发测试时,当并发数达到最大时,新的并发测试需要等待,那么等待期间的时间消耗不能统计到测试的耗时中,所以需要先计算当前耗时,在真正被并发调度后才清空t.start以跳过等待时间。

看过前面的Run()方法实现机制后,我们知道一旦子测试以并发模式执行时,需要通知父测试,其通知机制便是向t.signal管道中写入一个信号,父测试便从Run()方法中唤醒,继续执行。

看过前面的tRunner()方法实现机制后,不难理解,父测试唤醒后继续执行,结束后进入defer流程中,在defer中将启动所有子测试并等待子测试执行结束。

完整的测试执行:tRunner()

与简单版的测试执行所不同的是,defer语句中增加了子测试、并发测试的处理逻辑,相对完整的tRunner()代码如下所示:

  1. func tRunner(t *T, fn func(t *T)) {
  2. t.runner = callerName(0)
  3. defer func() {
  4. t.duration += time.Since(t.start) // 进入defer后立即记录测试执行时间,后续流程所花费的时间不应该统计到本测试执行用时中
  5. if len(t.sub) > 0 { // 如果存在子测试,则启动并等待其完成
  6. t.context.release() // 减少运行计数
  7. close(t.barrier) // 启动子测试
  8. for _, sub := range t.sub { // 等待所有子测试结束
  9. <-sub.signal
  10. }
  11. if !t.isParallel { // 如果当前测试非并发模式,则等待并发执行,类似于测试函数中执行t.Parallel()
  12. t.context.waitParallel()
  13. }
  14. } else if t.isParallel { // 如果当前测试是并发模式,则释放信号以启动新的测试
  15. t.context.release()
  16. }
  17. t.report() // 测试执行结束后向父测试报告日志
  18. t.done = true
  19. t.signal <- signal // 向父测试发送结束信号,以便结束Run()
  20. }()
  21. t.start = time.Now() // 记录测试开始时间
  22. fn(t)
  23. // code beyond here will not be executed when FailNow is invoked
  24. t.finished = true
  25. }

测试执行结束,进入defer后需要启动子测试,启动方法为关闭t.barrier管道,然后等待所有子测试执行结束。

需要注意的是,关闭t.barrier管道,阻塞在t.barrier管道上的协程同样会被唤醒,也是发送信号的一种方式,关于管道的更多实现细节,请参考管道实现原理相关章节。

defer中,如果检测到当前测试本身也处理并发中,那么结束后需要释放一个信号(t.context.release())来启动一个等待的测试。