Mocking system time in tests
Introduction
Recently I was working on my personal project and I encountered a situation where I was calling time.Now()
to get the current system time. I was working on a function to calculate someone's age from a birthday, I needed this function to display it in my app. I did not want another field in my database with someone's age, since in my opinion this calculation from the birthday is pretty trivial.
Coding
Since the function to calculate the age is a function with a clear in and output we could easily test it using table driven tests. If you don't know what table tests this is a good explanation. The benefits of using table driven tests are that adding and adjusting test cases is fairly easy, which is exactly what I needed to test my Age
function.
My implementation of the Age
function looks like this.
package account
import (
"time"
)
type UserProfile struct {
Birthday time.Time
// other fields omitted
}
func (up UserProfile) Age() uint {
now := time.Now()
currentYear := now.Year()
// if the current year is equal then or lower then the birthday it's always zero
// because no years have passed
if currentYear <= up.Birthday.Year() {
return 0
}
difference := currentYear - up.Birthday.Year()
// if the birthday of the user profile is higher then the current month
// we know the user birthday hasn't been yet.
if up.Birthday.Month() > now.Month() {
return uint(difference - 1)
}
// check if the birthday is the same as the current month
// and if the day is later in the month then the current day
if monthDayIsBefore(up.Birthday, now) {
return uint(difference - 1)
}
return uint(difference)
}
func monthDayIsBefore(t, s time.Time) bool {
return t.Month() == s.Month() && t.Day() > s.Day()
}
When I started with the tests I soon found out that I needed a way to control the time I was comparing against. For example, with the current implementation there is no way to control the now
variable.
This variable always depends on the system time, I found this to be bad for reproducibility of the tests.
The only assertion I could made against the return value was that the value is bigger then 0. There was no actual way to test the age, without updating the tests all the time.
I needed a way to mock the now
variable. The part I needed to mock was the call to time.Now()
, because this one was using the implementation from the standard library which uses the system time underneath.
There are multiple ways to abstract this behaviour but I choose to go for the interface way.
I defined the following interface
package clock
import (
"time"
)
type Clock interface {
Now() time.Time
}
Instead of calling time.Now()
I passed this interface as a parameter to the age function and called the Now()
function from the interface.
Note that instead of passing it as a parameter it could also be part of the struct.
func (up UserProfile) Age(c clock.Clock) uint {
now := c.Now()
// rest omitted for clarity
}
Now the function itself is fully under our control, it doesnt depend on any external factors like the Now
function from the time
package.
I made 2 implementations based on the Clock
interface, one which uses the standard library time.Now()
and one which uses a fixed time for in the tests.
package clock
import (
"time"
)
type Mock time.Time
func (m Mock) Now() time.Time { return time.Time(m) }
type Real struct{}
func (Real) Now() time.Time { return time.Now() }
In the tests we pass the Mock
type with the fixed time and in the business logic we use the Real
implementation.
My tests started looked like the following.
package account
import (
"testing"
)
func TestAge(t *testing.T) {
testCases := []struct {
desc string
profile UserProfile
clock clock.Clock
expectedAge uint
}{
{
desc: "Test with birthday in the future",
profile: UserProfile{
Birthday: time.Date(2011, 1, 1, 1, 1, 1, 1, time.UTC),
},
clock: clock.Mock(time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)),
expectedAge: 0,
},
{
desc: "Test with birthday in the same year but in the past",
profile: UserProfile{
Birthday: time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC),
},
clock: clock.Mock(time.Date(2010, 2, 1, 1, 1, 1, 1, time.UTC)),
expectedAge: 0,
},
// cases omitted for clarity
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
if tC.profile.Age(tC.clock) != tC.expectedAge {
t.Errorf("Expected profile age %v to be equal to expected age %v", tC.profile.Age(tC.clock), tC.expectedAge)
}
})
}
}
Cleaning up the public API
Right now the dependencies are really clear and in the public exported API. For the caller of the Age
function this might be a little bit to much, you
might expect that just calling Age
will just return the actual Age
compared to the current time. Now something which is only useful for testing is leaked in the public API.
We can fix this with a private helper function.
package account
type UserProfile struct {
// fields omitted for clarity
}
func (up UserProfile) Age() uint {
return up.age(clock.Real{})
}
func (up UserProfile) age(c clock.Clock) uint {
// implementation of age function
}
In the tests we test the private age
function, which contains the actual age calculation. The exported Age
function just propagates the call to the private function with the real system time implementation.
Conclusion
With this approach we are able to control the exact values used in the Age
function, to assert the expected behaviour. On actual runtime we still have the flexibility to use the system clock but with the downside that we have to pass it as a parameter to the age function.
The example shown in the blog post is a very simplistic example, but the concept can be used in many different places and not only for mocking out time.