Testing Patterns
This guide covers common testing patterns for time-dependent code.
Basic Time Control
The fundamental pattern: create a FakeClock, advance time, verify behavior.
func TestTimeoutBehavior(t *testing.T) {
clock := clockz.NewFakeClockAt(time.Now())
done := make(chan bool)
go func() {
<-clock.After(5 * time.Minute)
done <- true
}()
// Verify nothing happens before timeout
clock.Advance(4 * time.Minute)
select {
case <-done:
t.Fatal("Should not timeout yet")
default:
// Expected
}
// Trigger timeout
clock.Advance(1 * time.Minute)
select {
case <-done:
// Success
case <-time.After(100 * time.Millisecond):
t.Fatal("Should have timed out")
}
}
Testing Concurrent Timers
When testing multiple timers, they fire in chronological order as time advances:
func TestConcurrentTimers(t *testing.T) {
clock := clockz.NewFakeClockAt(time.Now())
results := make(chan int, 3)
// Start timers with different durations
go func() {
<-clock.After(1 * time.Second)
results <- 1
}()
go func() {
<-clock.After(2 * time.Second)
results <- 2
}()
go func() {
<-clock.After(3 * time.Second)
results <- 3
}()
// Allow goroutines to register their timers
time.Sleep(10 * time.Millisecond)
// Advance past first two timers
clock.Advance(2 * time.Second)
if val := <-results; val != 1 {
t.Errorf("Expected 1, got %d", val)
}
if val := <-results; val != 2 {
t.Errorf("Expected 2, got %d", val)
}
// Advance past third timer
clock.Advance(1 * time.Second)
if val := <-results; val != 3 {
t.Errorf("Expected 3, got %d", val)
}
}
Context Timeout Testing
Test context deadlines without real waits:
func TestContextDeadline(t *testing.T) {
clock := clockz.NewFakeClockAt(time.Now())
ctx, cancel := clock.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
done := make(chan error)
go func() {
<-ctx.Done()
done <- ctx.Err()
}()
// Advance to trigger timeout
clock.Advance(5 * time.Second)
err := <-done
if err != context.DeadlineExceeded {
t.Errorf("Expected DeadlineExceeded, got %v", err)
}
}
Testing Periodic Tasks
Test ticker-based periodic work:
func TestPeriodicTask(t *testing.T) {
clock := clockz.NewFakeClockAt(time.Now())
executions := 0
ticker := clock.NewTicker(time.Minute)
defer ticker.Stop()
// Simulate 5 ticks
go func() {
for i := 0; i < 5; i++ {
<-ticker.C()
executions++
}
}()
// Advance through 5 minutes
for i := 0; i < 5; i++ {
clock.Advance(time.Minute)
time.Sleep(time.Millisecond) // Let goroutine process
}
if executions != 5 {
t.Errorf("Expected 5 executions, got %d", executions)
}
}
Synchronization with BlockUntilReady
When goroutines need time to set up their timers, use BlockUntilReady():
func TestWithSynchronization(t *testing.T) {
clock := clockz.NewFakeClockAt(time.Now())
done := make(chan bool)
go func() {
<-clock.After(5 * time.Second)
done <- true
}()
// Wait for timer to be registered
clock.BlockUntilReady()
// Now safe to advance
clock.Advance(5 * time.Second)
select {
case <-done:
// Success
case <-time.After(100 * time.Millisecond):
t.Fatal("Timer did not fire")
}
}
Testing Timer Reset
Verify timer reset behavior:
func TestTimerReset(t *testing.T) {
clock := clockz.NewFakeClockAt(time.Now())
timer := clock.NewTimer(5 * time.Second)
defer timer.Stop()
// Advance partway
clock.Advance(3 * time.Second)
// Reset extends from current time
timer.Reset(5 * time.Second)
// Original deadline (5s) has passed, but reset deadline (8s total) hasn't
clock.Advance(3 * time.Second) // Now at 6s total
select {
case <-timer.C():
t.Fatal("Timer should not have fired yet")
default:
// Expected - 2 more seconds until reset deadline
}
// Advance past reset deadline
clock.Advance(2 * time.Second) // Now at 8s total
select {
case <-timer.C():
// Success
case <-time.After(100 * time.Millisecond):
t.Fatal("Timer should have fired")
}
}
Common Pitfalls
Pitfall: Forgetting to Stop Timers
// Bad - timer leaks
timer := clock.NewTimer(5 * time.Second)
// ... use timer
// Good - always stop
timer := clock.NewTimer(5 * time.Second)
defer timer.Stop()
Pitfall: Not Waiting for Goroutines
// Bad - race between goroutine setup and Advance
go func() {
<-clock.After(time.Second)
}()
clock.Advance(time.Second) // May advance before timer is registered
// Good - allow setup time or use BlockUntilReady
go func() {
<-clock.After(time.Second)
}()
clock.BlockUntilReady()
clock.Advance(time.Second)
Pitfall: Not Calling Context Cancel
// Bad - resources leak
ctx, _ := clock.WithTimeout(ctx, time.Second)
// Good - always call cancel
ctx, cancel := clock.WithTimeout(ctx, time.Second)
defer cancel()