• 简介
  • 数据结构
  • testContext
    • 等待并发执行:testContext.waitParallel()
    • 并发测试结束:testContext.release()
  • 测试执行:tRunner()
  • 启动子测试:Run()
  • 启动并发测试:Parallel()
  • 完整的测试执行:tRunner()

    简介

    在了解过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())来启动一个等待的测试。