Introduction
Testing concurrent code in Go has always been challenging. This is especially true for test cases involving time-dependent behavior. Traditional testing methods often rely on the actual system clock and synchronization mechanisms, leading to slow and unreliable tests.
For instance, when testing the expiration feature of the go-cache library, a common approach would look like this:
go-cache is a popular Go caching library that supports time-to-live (TTL) and periodic cleanup of expired cache items. It is well-suited for caching data and automatically deletes expired entries.
List 1: TestGoCacheEntryExpires testing cache in the usual way
|
|
Running this test requires waiting for 5 seconds for the cache entry to expire:
|
|
The testing/synctest Approach
Starting with Go 1.24, the testing/synctest package significantly improves this process. Here’s how the same test case can be rewritten using testing/synctest
Test Example 2: TestGoCacheEntryExpiresWithSynctest testing cache in the synctest
|
|
Running this test
|
|
The test now completes in just 0.009s.
Why is it so fast?
Unveiling testing/synctest
testing/synctest simplifies testing concurrent code by using virtual clocks and goroutine groups (also known as “bubbles”). This makes tests both fast and reliable. The package offers two core functions:
|
|
Key Features
- Run: Executes the provided function
fin a newgoroutineunder its bubble, ensuring that the virtual clock controls all relatedgoroutines. - Wait: Synchronizes the bubble’s
goroutinesand blocks the maingoroutineuntil all others are in adurably blockedstate.
A durably blocked state occurs when goroutines are waiting on:
- Channel send/receive within the bubble.
selectstatements involving bubble channelssync.Cond.Waittime.Sleep
However,goroutinesblocked by system calls or external events are not considereddurably blocked.
Virtual Clocks
Each bubble has a virtual clock starting from 2000-01-01 00:00:00 UTC. The clock advances only when all goroutines are idle. This allows time.Sleep calls will be completed instantly if all goroutines are idle, significantly speeding up tests.
List 3: TestSynctest Virtual Clocks are not affected by program run time
|
|
Output:
|
|
Even computationally intensive functions do not affect the virtual clock’s time.
Conclusion
The testing/synctest package introduces a powerful way to test concurrent Go code efficiently and reliably by leveraging virtual clocks and goroutine control. It eliminates the need for real-time waiting and makes time-dependent tests faster and more predictable.