Is `time.Now()` Wrecking Your Go Tests? Here's the Fix
Tired of flaky Go tests? Learn why `time.Now()` might be the culprit and discover simple, effective dependency injection patterns to make your tests deterministic.
Daniel Ivanov
Senior Go developer passionate about writing clean, testable, and maintainable code.
You’ve been there. You push your code, all tests are green. The next morning, you wake up to a frantic Slack message: the build is broken. A test that passed yesterday is now failing for no apparent reason. You re-run the pipeline, and it passes. What gives?
This is the maddening world of flaky tests. And one of the most common, yet overlooked, culprits in Go is a function you probably use every day: time.Now()
.
It seems harmless, but calling time.Now()
directly inside your application logic is a ticking time bomb for your test suite. Let's defuse it.
The Problem: Why `time.Now()` is a Test Killer
The foundation of a good unit test is determinism. Given the same input, a test should always produce the same output. It should pass or fail consistently, every single time.
time.Now()
shatters this principle. By its very definition, it returns a different value every time you call it. It’s a function with a hidden input—the state of the physical world—making it non-deterministic.
When your code's behavior depends on the current time, and you call time.Now()
directly, you lose control. Your tests become coupled to the environment they run in, not just the logic they are supposed to verify.
A Simple, Flaky Example
Imagine we have a service that determines if a product is eligible for a "New Arrival" promotion. The rule is simple: the product must have been created within the last 48 hours.
Here’s our initial code:
// promotion/service.go
package promotion
import "time"
type Product struct {
ID string
CreatedAt time.Time
}
// IsNewArrival checks if a product was created in the last 48 hours.
func IsNewArrival(p Product) bool {
durationSinceCreation := time.Now().Sub(p.CreatedAt)
return durationSinceCreation < 48*time.Hour
}
Now, let's try to write a test for this. A naive approach might look like this:
// promotion/service_test.go
package promotion
import (
"testing"
"time"
)
func TestIsNewArrival_Flaky(t *testing.T) {
// This product was created just a moment ago.
p := Product{ID: "abc-123", CreatedAt: time.Now()}
if !IsNewArrival(p) {
t.Errorf("Expected product to be a new arrival, but it wasn't")
}
}
This test will probably pass. But it tells us very little. We can't test the most important cases:
- What happens for a product created exactly 47 hours and 59 minutes ago? (Should be true)
- What happens for a product created exactly 48 hours and 1 second ago? (Should be false)
We can't reliably create these conditions because our test execution time is unpredictable, and time.Now()
is always moving forward. We are testing against a moving target.
The Solution: Treat Time as a Dependency
The fix is to reframe the problem. time.Now()
isn't just a utility function; it's an external dependency, just like a database or a third-party API.
If you can't control a dependency in your tests, you should abstract it away.
By applying the principle of Dependency Injection, we can provide a "real" clock for our application and a "fake" or "mock" clock for our tests. This gives us complete control over time in our test environment.
Step 1: Define a Clock Interface
First, we create a simple interface that abstracts the action of getting the current time.
// clock/clock.go
package clock
import "time"
// Clock provides an interface for getting the current time.
// This allows for dependency injection in tests. ype Clock interface {
Now() time.Time
}
Step 2: Create a Production Clock
Next, we create a concrete implementation of this interface that we'll use in our production code. This struct simply wraps the standard library's time.Now()
.
// clock/clock.go
package clock
import "time"
// RealClock is the production implementation of the Clock interface.
type RealClock struct{}
// Now returns the current local time.
func (rc RealClock) Now() time.Time {
return time.Now()
}
Step 3: Refactor the Business Logic
Now, we update our IsNewArrival
function to accept our new Clock
interface as an argument instead of calling time.Now()
directly.
// promotion/service.go
package promotion
import (
"time"
"your-project/clock" // Import your new clock package
)
// ... Product struct is the same ...
// IsNewArrival now accepts a Clock, making it deterministic.
func IsNewArrival(p Product, c clock.Clock) bool {
durationSinceCreation := c.Now().Sub(p.CreatedAt)
return durationSinceCreation < 48*time.Hour
}
Our function's signature has changed, but its core logic remains the same. The crucial difference is that it no longer has a hidden dependency; all its inputs are now explicit.
Writing Rock-Solid, Deterministic Tests
With our refactoring complete, writing tests becomes a joy. We can create a MockClock
that allows us to set the "current" time to whatever we want.
Step 4: Create a Mock Clock for Testing
Inside our test file (or a dedicated test helpers package), we create a mock implementation of our Clock
interface.
// promotion/service_test.go
package promotion
import "time"
// MockClock is a test implementation of the Clock interface.
// It allows us to manually set the time.
type MockClock struct {
CurrentTime time.Time
}
// Now returns the time set in CurrentTime.
func (mc *MockClock) Now() time.Time {
return mc.CurrentTime
}
Step 5: Write Comprehensive Tests
Now we can test our logic against any scenario we can imagine, with perfect precision and repeatability.
// promotion/service_test.go
package promotion
import (
"testing"
"time"
"your-project/clock"
)
// ... MockClock implementation from above ...
func TestIsNewArrival(t *testing.T) {
// 1. Arrange: Set a fixed point in time for our test's "now".
fixedTime := time.Date(2025, 1, 15, 10, 0, 0, 0, time.UTC)
mockClock := &MockClock{CurrentTime: fixedTime}
testCases := []struct {
name string
product Product
expected bool
}{
{
name: "Eligible: created 24 hours ago",
product: Product{
ID: "p1",
CreatedAt: fixedTime.Add(-24 * time.Hour),
},
expected: true,
},
{
name: "Ineligible: created 50 hours ago",
product: Product{
ID: "p2",
CreatedAt: fixedTime.Add(-50 * time.Hour),
},
expected: false,
},
{
name: "Edge Case: created exactly 48 hours ago",
product: Product{
ID: "p3",
CreatedAt: fixedTime.Add(-48 * time.Hour),
},
expected: false, // .Sub() result is 48h, which is not < 48h
},
{
name: "Edge Case: created just under 48 hours ago",
product: Product{
ID: "p4",
CreatedAt: fixedTime.Add(-48*time.Hour).Add(1 * time.Second),
},
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 2. Act: Call the function with the product and our mock clock.
got := IsNewArrival(tc.product, mockClock)
// 3. Assert: Check if the result is what we expect.
if got != tc.expected {
t.Errorf("IsNewArrival() = %v; want %v", got, tc.expected)
}
})
}
}
Look at how clear and robust these tests are! They will pass today, tomorrow, and a year from now, regardless of when they are run. We have successfully eliminated the non-determinism.
Conclusion: Take Control of Time
The allure of time.Now()
is its simplicity, but that simplicity hides a cost: test fragility. By treating time as just another dependency, you empower yourself to write cleaner, more predictable, and more resilient code.
The next time you find yourself reaching for time.Now()
, pause and ask: "Does my logic depend on this?" If the answer is yes, take a few extra minutes to inject a Clock
. Your future self—and your team—will thank you when the build stays green.