Time-dependent code is one of the classic pain points in unit testing. A function that calls time.Now() produces a different value every run, and assertions become a moving target. There are popular packages to solve this — but you don’t actually need any of them. A few lines of plain Go are enough.
The Problem
Anything that touches time.Now() directly is hard to test:
func (d *Date) IsExpired() bool {
return time.Now().After(d.deadline)
}
You can’t pin the clock from the test, so you either skip the assertion, accept flakiness, or reach for a mocking library that wraps the entire time package.
The Fix: Inject the Clock as a Function
Replace the direct call with a function field on the struct:
package mylib
import (
"time"
)
type Date struct {
now func() time.Time
// ...
}
The factory wires it to time.Now by default:
func NewDate() *Date {
return &Date{
now: time.Now,
// ...
}
}
In production, d.now() behaves exactly like time.Now(). There’s no indirection cost worth worrying about, and the public API doesn’t change.
Freezing Time in a Test
Inside the test, override now with a closure that returns whatever instant you need:
func TestDate_IsExpired(t *testing.T) {
d := NewDate()
d.now = func() time.Time {
return time.Date(2020, 5, 11, 2, 1, 50, 0, time.UTC)
}
// assertions against a fixed clock...
}
That’s it. No package, no global state, no monkey-patching.
Black-Box Testing? Expose It
If you write your tests in a separate mylib_test package (black-box testing), the lowercase now field isn’t reachable. Two clean options:
Option 1 — export the field:
type Date struct {
Now func() time.Time
}
Option 2 — provide a setter or a functional option:
func WithClock(now func() time.Time) Option {
return func(d *Date) { d.now = now }
}
d := NewDate(WithClock(func() time.Time {
return time.Date(2020, 5, 11, 2, 1, 50, 0, time.UTC)
}))
The second form keeps the field private and gives you a clean injection point — the same pattern you already use for everything else configurable.
Why This Beats a Library
- Zero dependencies. No
clock, noquartz, no transitive surprises. - Local reasoning. The dependency is visible in the struct definition. There’s no hidden global to remember to reset between tests.
- Composable. You can advance the clock, freeze it, return a sequence of times — anything a closure can do.
- Idiomatic. It’s the same dependency-injection pattern Go developers already use for
io.Reader, loggers, and HTTP clients.
A func() time.Time field is the smallest interface for “give me the current time,” and it’s all most code ever needs. Master time, skip the library.