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 are here 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.