What does testing/synctest's Test function give a Go test that real time.Sleep calls cannot?
answer
- the test body runs inside a bubble
- wall-clock time is not what advances
- the clock jumps to the next timer
- a five-minute TTL in microseconds
basics
~20 ssynctest.Test runs a test inside a bubble with a fake clock, so time.Sleep, timers and tickers jump forward instantly instead of really waiting. A five-minute expiry can be tested in microseconds, with no slow-machine flakes.
solid answer
~40 s`synctest.Test(t, f)` runs `f` in a bubble: the goroutine running `f` plus every goroutine started from inside it. Within that bubble the `time` package is virtualised — `time.Now`, `time.Sleep`, timers, tickers and the deadlines of contexts created inside all read a fake clock. That clock only moves when every goroutine in the bubble is blocked in a way that only another bubbled goroutine can undo, and then it jumps straight to the earliest pending timer. So a test can sleep for a virtual hour and finish in microseconds, and the sequence of events is exactly the one the code would see in production. A real `time.Sleep` in a test buys neither: it costs wall-clock time and still fails on a loaded CI machine. The package is generally available from Go 1.25.
code
go · 16 linesfunc TestCacheEntryExpires(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
c := newCache(5 * time.Minute)
c.Set("k", "v")
time.Sleep(4 * time.Minute)
if _, ok := c.Get("k"); !ok {
t.Fatal("entry expired early")
}
time.Sleep(2 * time.Minute)
if _, ok := c.Get("k"); ok {
t.Fatal("entry outlived its TTL")
}
})
}go deeper
Be ready to say what synctest.Test does in one sentence: it runs the test body in a bubble with a fake clock, so sleeps and timers cost no real time. Naming one thing it makes testable, such as an expiry, is enough here.
An interviewer expects the mechanism: which goroutines join the bubble, which time calls are virtualised, and the rule that the clock only advances when every bubbled goroutine is blocked on something inside the bubble.
Show where the technique stops. Real I/O and pre-existing goroutines are not bubbled, the bubble controls time rather than interleaving, and a test that reaches outside it will hang rather than run fast.
Be ready to argue what the team gains by adopting it: time-driven tests stop being a source of flakes and stop paying wall-clock cost, at the price of a Go 1.25 floor and of keeping test dependencies in-memory.
## The problem this solves Go code is full of durations. A cache entry expires after five minutes. A refresh ticker fires every thirty seconds. A retry backs off for two seconds, then four. Testing that behaviour honestly means letting time pass, and a suite that lets real time pass is both slow and unreliable: sleep a little and a loaded CI box fails the assertion; sleep a lot and the suite takes minutes to run. `testing/synctest`, generally available since Go 1.25, deletes the waiting rather than tuning it. ## What a bubble is `synctest.Test(t, f)` runs `f` in a *bubble*. The bubble consists of the goroutine running `f` and every goroutine started from inside it, directly or transitively. Membership is by ancestry: a goroutine that already existed before the call is not in the bubble, and neither is anything else the process is doing. Inside the bubble the `time` package is replaced by a virtual implementation. `time.Now`, `time.Since`, `time.Sleep`, `time.After`, `time.NewTimer`, `time.NewTicker`, `time.AfterFunc`, and the deadlines of `context.WithTimeout` / `context.WithDeadline` values created inside the bubble all read the bubble's own clock. That clock starts at a fixed instant and is driven by the runtime, not by the machine. ## How the clock moves The rule is short and it is the whole model: **the bubble's clock advances only when every goroutine in the bubble is durably blocked, and it then jumps directly to the earliest pending timer deadline.** "Durably blocked" means blocked such that only another goroutine in the same bubble could unblock it — sleeping, waiting on a channel created inside the bubble, waiting in `sync.WaitGroup.Wait` or `sync.Cond.Wait`. When nothing in the bubble can make progress and a timer is outstanding, waiting is pointless, so the runtime simply moves the clock to that timer and lets it fire. The consequence for a test author is that `time.Sleep(24 * time.Hour)` returns essentially at once, while every observation the program makes is consistent with a day having passed: `time.Now` differences, expiry comparisons, and every ticker tick that would have occurred in between. ## What it looks like in practice A test for a cache whose entries live five minutes can be written as the story it actually is: put a value in, sleep four minutes, assert it is still there, sleep two more, assert it is gone. No padding, no scaling constant, no `-short` skip. It runs faster than a test that sleeps ten milliseconds "to be safe", and it is deterministic: there is no wall clock left to be slow. ## What it does not do It does not fake the CPU. Computation inside the bubble takes the real time it takes; only the *clock the program reads* is virtual. It does not virtualise the outside world. Real network reads, file I/O, syscalls and channels created outside the bubble are still real, and a goroutine sitting in one of them is not durably blocked — so the bubble never settles and the clock never advances. Tests written for a bubble keep their dependencies inside it, using in-memory transports and channels created within `f`. It does not make concurrency bugs disappear. The bubble controls *time*, not the interleaving of runnable goroutines; the race detector is still the tool for races. And the bubble has an exit contract: every goroutine started inside it must have exited when `f` returns, or `synctest.Test` panics rather than letting a stray goroutine escape. ## Versions Go 1.24 shipped an experimental version of the package, entered through `synctest.Run(f func())` and gated behind `GOEXPERIMENT=synctest`. Go 1.25 made the package generally available with the `synctest.Test(t, f)` entry point, which ties the bubble to a `*testing.T`. Go 1.27 added `synctest.Sleep` and an in-memory `net/http/httptest.NewTestServer` intended for bubbled tests. If your module builds on Go 1.25 or later you can rely on `synctest.Test`. ## Why interviewers like this question It separates candidates who treat `time.Sleep` as a synchronisation primitive from those who see time as an input a test should control. The follow-up is almost always "and what stops the bubble from advancing?", which is where the durable-blocking rule has to come out.
- Which time-related operations inside the bubble actually use the fake clock?`time.Now`, `time.Since`, `time.Sleep`, `time.After`, `time.NewTimer`, `time.NewTicker` and `time.AfterFunc`, plus the deadlines of contexts created inside the bubble. Code running outside the bubble keeps reading the real clock, which is one reason a bubbled test should not share timers with goroutines started before the call.
- Which Go version can you rely on for synctest.Test, and what came before it?Go 1.25 made `testing/synctest` generally available with `synctest.Test(t, f)`. Go 1.24 had an experimental predecessor, `synctest.Run(f func())`, gated behind `GOEXPERIMENT=synctest`. Go 1.27 added `synctest.Sleep`. On anything older there is no bubble at all.
- Does the bubble make the test's computation faster too?No. Only the clock the program reads is virtual; CPU work takes the time it takes. What disappears is idle waiting — sleeps, timer deadlines and ticker periods — which is where the minutes in a time-driven suite normally go.
It is a flight simulator for time: the program experiences a full six-hour flight — every timer, every expiry, in the right order — while the people running the test never wait six hours.
saying these in an interview costs you the question
- Says synctest speeds tests up by shrinking durations by a fixed factor
- Thinks the fake clock ticks on its own in the background
- Claims you still need a small padding sleep for reliability
- Believes goroutines started before the call also see fake time
- Assumes the bubble also makes computation inside it faster