Go 异步测试引入 synctest,终于舒服点了。。。

tou shi

在 Go 语言里,写并发代码是日常操作。但只要涉及到 测试异步函数,很多人都会头大。为什么?因为这不是你调用完就能立马拿到结果的同步函数,而是活还没干完,就已经收工了。
Go1.25 正式引入的 testing/synctest 包,正是为了解决这个问题。它能帮我们用更快、更可靠的方式测试并发和异步代码。
我将结合官方案例和代码,聊聊这个包解决了什么痛点,以及分享带来的新用法。

什么是异步函数?

先来对比一下。
同步函数很好理解:调用它 → 执行任务 → 返回。比如一个删除缓存目录的函数:

go Copy
func (c *Cache) Cleanup() {
    os.RemoveAll(c.cacheDir)
}

异步函数就不一样了:你调用它 → 它马上返回 → 后台再慢慢处理。

go Copy
func (c *Cache) CleanupInBackground() {
    go os.RemoveAll(c.cacheDir)
}

再比如 context.WithDeadline,它会返回一个 context,然后在未来的某个时间点自动取消:

go Copy
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

像这种 和时间、并发挂钩的逻辑,测试起来就是 “灾难”。

为什么测试异步很难?
例子

测试同步函数,套路很简单:

  1. 设置初始条件;
  2. 调用函数;
  3. 校验结果。
    但测试异步函数呢?
  4. 你调用了函数;
  5. 它提前返回;
  6. 你得等它真正执行完;
  7. 然后才能校验结果。
    问题
    问题来了:
  8. **等多久才合适?**如果等太短,可能还没执行完;等太长,测试又变得很慢。
  9. 怎么保证“不发生”的事? 比如我要验证 deadline 之前 context 没被取消,这个“没发生”怎么测?
    结果就是:要么慢,要么不稳定。CI 跑一遍还好,跑多了不是超时就是有其他问题。

传统做法:Sleep + “好运”
我们写个测试,验证 context.WithDeadline 在 deadline 后被取消。
例如:

go Copy
func TestWithDeadlineAfterDeadline(t *testing.T) {
    deadline := time.Now().Add(1 * time.Second)
    ctx, _ := context.WithDeadline(t.Context(), deadline)

    time.Sleep(time.Until(deadline))

    if err := ctx.Err(); err != context.DeadlineExceeded {
        t.Fatalf("context not canceled after deadline")
    }
}

看似合理,但问题很明显:
1.** 太脆弱**:等到 deadline 那一刻,context 可能还没取消完。
2. 太慢:这个测试要跑 1 秒多,一个简单 case 占用这么久,很不划算。
大家都知道。我们最常见的改法是加点 buffer,比如等 1.1s:

go Copy
  time.Sleep(time.Until(deadline) + 100*time.Millisecond)

但这依然不靠谱。在本机可能 100ms 很长,在 CI 里遇上调度卡顿就不够了。
于是陷入经典困境:慢 或 不稳定,二选一。

synctest:测试里的 “bubble”
Go 团队最后给出的解法是:testing/synctest。已经在本次 Go1.25 的新特性中引入。
它的核心思想是:在测试里开一个 “bubble”,里面的时间是假的,执行逻辑是可控的。
API 只有两个:

go Copy
// 在 bubble 里执行测试函数
func Test(t *testing.T, f func(*testing.T))

// 等待 bubble 内的 goroutine 全部 quiesce(进入稳定状态)
func Wait()

看个对比:还是测试 context.WithDeadline。
以前写法:

go Copy
time.Sleep(time.Until(deadline) + 100*time.Millisecond)

用 synctest 改造后:

go Copy
func TestWithDeadlineAfterDeadline(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        deadline := time.Now().Add(1 * time.Second)
        ctx, _ := context.WithDeadline(t.Context(), deadline)

        time.Sleep(time.Until(deadline))
        synctest.Wait()

        if err := ctx.Err(); err != context.DeadlineExceeded {
            t.Fatalf("context not canceled after deadline")
        }
    })
}

变化不大:加了 synctest.Test 和 synctest.Wait。
但效果大不同:

  1. 测试几乎瞬间完成,不会真的睡 1 秒。
  2. 不用 hack buffer,也不会 flaky。
  3. 更重要的是,不需要改动 context 的源码。

时间与等待:更强的控制力
假时间
在 bubble 里,时间从 2000-01-01 00:00:00 UTC 开始。你可以随意 sleep,它会马上跳过去。

go Copy
func TestSleep(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        start := time.Now()
        time.Sleep(10 * time.Second)
        t.Log(time.Since(start)) // 输出 10s,瞬间完成
    })
}

所以测试里再也不会有“等一分钟”的尴尬。

**等待 quiescence
**
synctest.Wait 会等所有 goroutine 都进入稳定状态。比如:

go Copy
func TestWait(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        done := false
        go func() { done = true }()
        synctest.Wait()
        t.Log(done) // true
    })
}

如果不加 Wait,可能会出现数据竞争。但 Wait 提供了隐式同步,连 -race 检测器都能识别。

实战例子:测试 io.Copy
即便是 io.Copy 这种看似同步的函数,也存在异步行为。来看测试:

go Copy
func TestIOCopy(t *testing.T) {
    synctest.Test(t, func(t *testing.T) {
        srcReader, srcWriter := io.Pipe()
        defer srcWriter.Close()

        var dst bytes.Buffer
        go io.Copy(&dst, srcReader)

        data := "1234"
        srcWriter.Write([]byte(data))
        synctest.Wait()

        if got, want := dst.String(), data; got != want {
            t.Errorf("Copy wrote %q, want %q", got, want)
        }
    })
}

这里我们不用加锁,不用加 sync.WaitGroup,只要 synctest.Wait 就能保证数据写入完成。
测试能做到更简洁,语义更清晰。

限制与注意点

当然,synctest 不是万能的:

  1. 真实 I/O、syscall、cgo 调用不算“durably blocking”,没法完全模拟。
  2. mutex 也不会被当成 durable block,fake clock 不会在等锁时推进。
  3. 网络场景要用 fake network(比如 net.Pipe),不能依赖真实 socket。
    但对绝大多数异步逻辑,已经够用了。

总结
并发本身就不 “简单”,更别提测试了。以前我们只能在 慢 vs 不稳定 之间做选择。
现在有了 synctest,我们能写出 快且稳 的并发测试。
它的核心就是:bubble。在这个 bubble 里,时间是假的,等待是可控的。

Update Time:Feb 04, 2026

Comments

Tips: Support some markdown syntax: **bold**, [bold](xxxxxxxxx), `code`, - list, > reference