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
f
in a newgoroutine
under its bubble, ensuring that the virtual clock controls all relatedgoroutines
. - Wait: Synchronizes the bubble’s
goroutines
and blocks the maingoroutine
until all others are in adurably blocked
state.
A durably blocked
state occurs when goroutines
are waiting on:
- Channel send/receive within the bubble.
select
statements involving bubble channelssync.Cond.Wait
time.Sleep
However,goroutines
blocked 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 to 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.