G

[Golang] 小试testing/synctest

RoLingG Golang 2025-11-30

1.25单元测试新增包testing/synctest

下面给出一段可运行的最小示例,用来体验 Go 1.25 新增的 testing/synctest 包。
代码只有 60 行,但完整展示了官方文档里提到的三大卖点:

  1. 隔离“气泡”——synctest.Test 把并发逻辑封进独立调度器;
  2. 虚拟时间——time.Sleep 不再真睡,而是在所有 goroutine 阻塞时瞬间“快进”;
  3. 等待机制——synctest.Wait 阻塞到所有 goroutine 都卡住,再断言时间。

代码:synctest_demo.go

package main

import (
    "sync"
    "testing"
    "time"

    // Go 1.25 新增包
    "testing/synctest"
)

// 要测的并发函数:启动 3 个 worker,每个睡 1 s,然后累加结果。
func ConcurrentCounter(done chan<- int) {
    var wg sync.WaitGroup
    sum := 0
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(v int) {
            defer wg.Done()
            time.Sleep(time.Second) // 这里是关键:会触发虚拟时间
            sum += v
        }(i + 1)
    }
    wg.Wait()
    done <- sum
}

// 单元测试
func TestConcurrentCounter(t *testing.T) {
    // 1. 把测试关进“气泡”
    synctest.Test(func(t *testing.T) {
        done := make(chan int, 1)

        // 2. 启动被测代码
        go ConcurrentCounter(done)

        // 3. 等待所有 goroutine 阻塞(这里会阻塞在 wg.Wait() 内部)
        synctest.Wait()

        // 4. 此时虚拟时间已经过去 1 s,但真实时间几乎没走
        select {
        case n := <-done:
            if n != 6 { // 1+2+3
                t.Fatalf("got %d; want 6", n)
            }
        default:
            t.Fatal("counter not finished")
        }
    }, t) // 把外层 t 传进去,失败时能正确汇报
}

// 可执行入口,方便 go run 直接看效果
func main() {
    testing.Main(func(pat, str string) (bool, error) { return true, nil },
        []testing.InternalTest{{Name: "TestConcurrentCounter", F: TestConcurrentCounter}},
        nil, nil)
}

运行方式(Go 1.25+):

go run synctest_demo.go -test.v

输出示例(真实耗时 < 5 ms):

=== RUN   TestConcurrentCounter
--- PASS: TestConcurrentCounter (0.00s)
PASS
ok      xxx    0.003s

代码逐段分析 & 官方特性映射

  1. 隔离“气泡”
    synctest.Test(f func(*testing.T), t *testing.T)

    • 内部会新建一个微型调度器,所有 goroutine 只在它内部运行;
    • 失败时把子失败关联到外层 t,与 t.Run 体验一致;
    • 官方文档强调“bubble”——这里正是这个术语的代码映射。
  2. 虚拟时间
    被测函数里写了 time.Sleep(time.Second),但在气泡内:

    • 时钟不会真睡;
    • 只有当所有 goroutine 都阻塞(这里是 wg.Wait())时,运行时自动把虚拟时钟拨到下一个定时器到期点;
    • 因此 1 s 的 Sleep 瞬间过去,真实时间几乎为 0。
  3. 等待机制
    synctest.Wait()

    • 阻塞调用者,直到当前气泡里再无可运行的 goroutine;
    • 在本例里,它等到三个 worker 都执行完 sum += vDone() 之后返回;
    • 返回后即可安全断言结果,无需再写 time.Sleep(…)sync.WaitGroup 在测试侧再套一层。

为什么这样设计(官方意图还原)

  • 传统测并发代码要么
    – 真睡(慢);
    – 或自己 mock time.After / time.Sleep(侵入业务代码)。
    synctest 把“时间”和“调度”同时虚拟化,做到零侵入 + 毫秒级
  • 过去想等“所有 goroutine 都卡住”必须手工 sync.WaitGroup 或轮询,现在一句 synctest.Wait() 即可,测试代码更短、更确定
  • 把失败链路挂到外层 t,则 go test-runfailfastjson 输出等生态完全复用,不需要新工具链

总结

最重要的就是这一句话 “synctest 把“时间”和“调度”同时虚拟化,做到零侵入 + 毫秒级。”

不用自己开个并发然后让主线程手动等待单元测试代码运行,synctest 包能够自动的让主线程等待单元测试。

并且 synctest 包的单元测试气泡里面的虚拟时钟不会真睡,这不仅能让开发人员费心去评估 sleep 的合适时间,也能让程序运行因为不用睡眠执行效率大大提升。

PREV
[分布式] CRAQ — 改进链复制
NEXT
[Golang] 1.25 encoding/json/v2更新

评论(0)

发布评论