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, no quartz, 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.