登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go 子测试调用 Parallel 后父测试什么时候继续执行

来源:17golang原创

时间:2026-10-06 13:47:38 420浏览 收藏

子测试调用 t.Parallel() 后,直接包住它的那次 t.Run 就可以解除阻塞,父测试函数会继续执行后面的代码。此时子测试在 t.Parallel 之后的部分处于暂停状态,通常要等父测试函数返回后才进入并行阶段;不过父测试整体还没有结束,它仍会等待所有子测试完成。

官方地址:https://pkg.go.dev/testing#T.Run

先理清三个不同的关键时点
  • t.Run 返回:子测试函数返回,或者子测试调用了 t.Parallel。
  • 父测试函数返回:父函数正文和普通 defer 都走完,并行子测试才有机会恢复。
  • 父测试完成:父函数已返回,而且它的全部子测试也都完成。

我最初混淆的是 Run 返回和测试完成

第一次读并行子测试时,我很容易把“t.Run 已经返回”理解成“子测试已经跑完”。真正的分界线是:T.Run 会在子测试函数返回,或者子测试调用 t.Parallel 变成并行测试时解除阻塞。后一个条件只表示子测试交出了当前执行位置,不表示它已经完成。

Go 官方还明确说明,父测试只有在全部子测试完成后才算完成。因此父代码会继续、子代码会暂停、父测试会等待,这三个状态可以同时成立。

Go 父测试、t.Run、t.Parallel 与并行子测试组的静态边界结构图
图1:父测试边界、子测试边界与调度屏障的静态结构说明图,不是测试运行截图。

用最小测试标出三个位置

下面的代码不依赖精确日志时间,只用三个位置帮助判断控制权在哪一侧。子测试先执行到 t.Parallel,随后直接 t.Run 可以返回,父函数继续;子测试中 t.Parallel 后面的断言则要等到父测试函数返回后再恢复。

package parallelparent

import "testing"

func TestParentContinues(t *testing.T) {
    t.Log("父测试:调用 Run 之前")

    t.Run("child", func(t *testing.T) {
        // 这行位于 Parallel 之前,会在 Run 解除阻塞前执行。
        t.Log("子测试:调用 Parallel 之前")

        t.Parallel()

        // 这部分先暂停,等父测试函数返回后再进入并行阶段。
        t.Log("子测试:Parallel 之后")
    })

    // 子测试调用 Parallel 后,父测试可以继续到这里。
    t.Log("父测试:Run 已返回,但子测试未必完成")
}

这里最有用的判断不是猜日志会不会紧挨着出现,而是看代码位置:Run 后的父代码不会等待并行子测试的后半段;整个 TestParentContinues 却会等子测试全部完成后才报告最终结果。

普通 defer 可能比并行子测试更早清理资源

这个语义最容易在共享资源上踩坑。父函数返回时会执行它自己的普通 defer,而直接子测试在 t.Parallel 之后还没有恢复。如果父函数用 defer 关闭数据库、HTTP 测试服务或临时依赖,子测试恢复后可能面对已经被关闭的资源。

我更倾向于把“测试树结束后再清理”的动作交给 t.Cleanup。官方契约是:Cleanup 注册的函数会在当前测试及其全部子测试完成后调用。

package parallelparent

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestSharedServer(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 测试服务只返回稳定状态,避免共享可变业务数据。
        w.WriteHeader(http.StatusNoContent)
    }))

    // Cleanup 会等当前测试及全部子测试完成后再关闭服务。
    t.Cleanup(server.Close)

    for _, name := range []string{"first", "second"} {
        name := name // 兼容旧版本 Go 的循环变量捕获方式。
        t.Run(name, func(t *testing.T) {
            t.Parallel()

            // 每个子测试创建自己的请求,只有服务地址被共享。
            req, err := http.NewRequest(http.MethodGet, server.URL, nil)
            if err != nil {
                t.Fatal(err)
            }
            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                t.Fatal(err)
            }
            defer resp.Body.Close() // 响应体只属于当前子测试。

            if resp.StatusCode != http.StatusNoContent {
                t.Fatalf("状态码 = %d,期望 %d", resp.StatusCode, http.StatusNoContent)
            }
        })
    }
}
Go 并行子测试中 defer、t.Cleanup 与分组 t.Run 的资源清理关系图
图2:普通 defer、t.Cleanup 和并行子测试完成条件的静态关系说明图,不是运行结果。

需要明确等待一组子测试时,加一层顺序分组

有时清理动作必须写在一段普通代码里,而不是注册成 Cleanup。此时可以增加一个不调用 t.Parallel 的分组子测试,让并行测试成为它的子节点。分组函数返回后,组内并行子测试开始恢复;外层那次 t.Run("parallel-group", ...) 要等组内子测试全部完成才返回,所以后面的清理代码拥有清楚的等待边界。

package parallelparent

import "testing"

func TestGroupedWorkers(t *testing.T) {
    resource := openResourceForTest()

    t.Run("parallel-group", func(t *testing.T) {
        for _, input := range []string{"a", "b", "c"} {
            input := input // 为每个子测试固定当前输入。
            t.Run(input, func(t *testing.T) {
                t.Parallel()

                // 组内子测试并行使用同一个只读测试资源。
                if err := resource.Check(input); err != nil {
                    t.Fatal(err)
                }
            })
        }
    })

    // 分组 Run 返回时,组内并行子测试已经全部完成。
    resource.Close()
}

// testResource 仅表示示例中的受控测试资源。
type testResource struct{}

func openResourceForTest() *testResource { return &testResource{} }

func (r *testResource) Check(input string) error {
    // 示例只展示父子测试边界,不引入额外业务逻辑。
    return nil
}

func (r *testResource) Close() {
    // 实际项目可在这里释放连接、文件或临时服务。
}

这个分组方式的价值在于局部:它只等待当前组里的并行子测试,不要求把整个顶层测试都改成串行清理脚本。

表驱动并行测试里我会保留的边界

  • Parallel 之前只做局部准备。 不要在这里修改其他并行子测试也会读写的全局状态。
  • 共享资源用并发安全或只读设计。 t.Parallel 解决的是调度,不会自动给业务对象加锁。
  • 测试树级清理优先用 t.Cleanup。 普通 defer 只绑定当前函数返回。
  • 需要等待一组子测试时用顺序分组。 让外层 Run 的返回成为清楚的组完成边界。
  • -parallel 只限制并行测试数量。 它不会改变父子测试的等待语义,也不保证具体调度顺序。

验收时只问这四个问题

检查点正确判断
直接 t.Run 何时返回子函数返回,或子测试调用 t.Parallel
Parallel 后的子代码何时恢复父测试函数返回、非并行阶段结束后
父测试何时算完成父函数返回且全部子测试完成后
共享资源何时清理用 t.Cleanup 等到测试树完成,或在分组 Run 返回后清理

常见问题

父测试在 t.Run 返回后能立刻读取子测试的最终结果吗? 不能这样假设。子测试可能只执行到 t.Parallel 就暂停,后半段尚未完成。

父测试函数返回后,父测试是不是就结束了? 不是。父测试整体还要等待所有子测试完成。

父测试里的 defer 适合关闭并行子测试共用的服务吗? 通常不适合,因为 defer 随父函数返回执行,可能早于并行子测试恢复。优先使用 t.Cleanup 或顺序分组。

把顶层测试也调用 t.Parallel 能解决等待问题吗? 不能。它会改变顶层测试与其他并行测试的关系,但不会取消父测试等待子测试完成的规则。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>