Testing in Go by example: Part 5

Michael Whatcott
Michael Whatcott
 • 
September 15, 2015
An elegant and effective strategy for dealing with time in unit tests
Tags

For this installment of the Testing in Go series I'll share a really nifty way to deal with time in your unit tests. When the behavior you are testing depends on the current time it can be tricky to assert on the results because the current time is a moving target. So, usually we end up resorting to approximations in our assertions that, while functional, always bother me a bit. In some cases, depending directly on the system's current time prevents acceptable test coverage.

Consider this trivial example, which defines a calendar service with a method that identifies the current quarter of the current calendar year:

File: calendar.go (version 1)
package calendar

import "time"

type Calendar struct {
}

func NewCalendar() *Calendar {
	return &Calendar{}
}

func (this *Calendar) CurrentQuarter() int {
	month := time.Now().UTC().Month()
	if month < 4 {
		return 1
	} else if month < 7 {
		return 2
	} else if month < 10 {
		return 3
	} else {
		return 4
	}
}

Here's an obvious test we could write:

File: calendar_test.go (version 1)
package calendar

import (
	"testing"
	"time"
)

func TestCurrentQuarter(t *testing.T) {
	expected := time.Now().UTC().Month()
	actual := NewCalendar().CurrentQuarter()
	if expected != actual {
		t.Error("Expected:", expected, "Actual:", actual)
	}
}

(sigh) That test certainly leaves something to be desired as it only exercises one branch of the if statement. What would you do to ensure that the logic is correct in all cases? How can we exercise each branch of the if statement?

The answer is not to run the test once every three months for a few years to see if the right values always come out. Only slightly better would be to run the test several times, modifying the system clock in between test runs (cringe). Here's a possible solution that doesn't require fiddling with any system-level resources or waiting for months to get the results:

File: calendar.go (version 2)
package calendar

import "time"

/////////////////////////////////////////////

func now = func() time.Time {
	return time.Now()
}

/////////////////////////////////////////////

type Calendar struct {
}

func NewCalendar() *Calendar {
	return &Calendar{}
}

func (this *Calendar) CurrentQuarter() int {
	month := now().UTC().Month()
	if month < 4 {
		return 1
	} else if month < 7 {
		return 2
	} else if month < 10 {
		return 3
	} else {
		return 4
	}
}

There's not much different there. The code still appears to be calling time.Now() to get the current time. But we've introduced a layer of indirection which the test code can put to good use:

File: calendar_test.go (version 2)
package calendar

import (
	"testing"
)

func TestCurrentQuarter(t *testing.T) {
	cases := []struct{
		month   string
		quarter int
	}{
		{month: "01", quarter: 1},
		{month: "02", quarter: 1},
		{month: "03", quarter: 1},
		{month: "04", quarter: 2},
		{month: "05", quarter: 2},
		{month: "06", quarter: 2},
		{month: "07", quarter: 3},
		{month: "08", quarter: 3},
		{month: "09", quarter: 3},
		{month: "10", quarter: 4},
		{month: "11", quarter: 4},
		{month: "12", quarter: 4},
	}

	for _, test := range cases {
		parsed, _ := time.Parse("2006-01-02", fmt.Sprintf("2015-%s-15", test.month))
		// IMPORTANT: we are swapping out the `now` function with something we control!
		now = func() time.Time { return parsed }
		calendar := NewCalendar()
		actual := cal.CurrentQuarter()
		if actual != test.quarter {
			t.Error("Month:", test.month, 
				"Expected Quarter:", test.quarter, 
				"Actual Quarter:", actual)
		}
	}
}

I learned this approach a few years ago when, as a .Net developer, I was referred to a helpful post that used a simple static class with a static Func field which would forward to the .Net platform's DateTime.UTCNow capability. In the test code it is trivial to reassign that static Func field to whatever would facilitate testing the behavior that was dependent on the current time.

This approach is simple. This approach is great. It probably works for most of the code out there that depends on the system time. But it breaks down if you ever want to run multiple test functions that depend on now() in parallel. Lately, we've been writing a lot of parallel tests because they help us drive out bugs related to shared, package-level state.

When I initially set out to engineer a solution that would allow the unit test to run in parallel I came up with something like this:

File: calendar.go (version 3)
package calendar

import (
	"time"
)

/////////////////////////////////////////////

type Clock interface {
	Now() time.Time
}

type Calendar struct {
	clock Clock
}

/////////////////////////////////////////////

func NewCalendar(clock Clock) *Calendar {
	return &Calendar{clock: clock}
}

func (this *Calendar) CurrentQuarter() int {
	month := this.clock.Now().UTC().Month()
	if month < 4 {
		return 1
	} else if month < 7 {
		return 2
	} else if month < 10 {
		return 3
	} else {
		return 4
	}
}

So, this approach requires two different implementations of the Clock interface. One is for use in production--it simply returns the result of time.Now() every time. The other could be implemented in a *_test.go file in the calendar package and would return a specified time.Time. This implementation would be provided to the Calendar constructor in the test code. I'll leave figuring out the details of those implementations and the necessary changes to the test code (calendar_test.go version 3) as an exercise to the reader.

This solution is pretty good. Just pass in a Clock wherever time.Now() is needed. Because the Calendar no longer depends on package-level state the tests can run in parallel. But...now there's an annoying 'constructor' parameter that has to be passed in. And now we have two clock implementations hanging around. Okay, how about this slight improvement:

File: calendar.go (version 4)
package calendar

import (
	"time"
)

/////////////////////////////////////////////

type Clock interface {
	Now() time.Time
}

type Calendar struct {
	clock Clock
}

/////////////////////////////////////////////

func NewCalendar() *Calendar {
	return &Calendar{clock: NewProductionClock()}
}

func (this *Calendar) CurrentQuarter() int {
	month := this.clock.Now().UTC().Month()
	if month < 4 {
		return 1
	} else if month < 7 {
		return 2
	} else if month < 10 {
		return 3
	} else {
		return 4
	}
}

Instead of having to pass in a clock to the 'constructor' we simply use the in-production version of the clock by default. The test code (because it belongs in the same package) can reassign the Calendar.clock variable with a "testing" clock instance. This simplifies production wireup, which is nice.

But wait! We can do even better by taking advantage of Go's interesting function/method model:

File: calendar.go (version 5)
package calendar

import (
	"time"
)

/////////////////////////////////////////////

type Clock struct {
	instant time.Time
}

func (this *Clock) Now() time.Time {
	if this == nil {
		return time.Now()
	}
	return this.instant
}

/////////////////////////////////////////////

type Calendar struct {
	clock *Clock
}

func NewCalendar() *Calendar {
	return &Calendar{}
}

func (this *Calendar) CurrentQuarter() int {
	month := this.clock.Now().UTC().Month()
	if month < 4 {
		return 1
	} else if month < 7 {
		return 2
	} else if month < 10 {
		return 3
	} else {
		return 4
	}
}

Did you catch the very suble difference? We are no longer using an interface for the clock. Now there's only one implementation--the one you see. Did you also notice that we aren't initializing the *Clock field on the Calendar? That's because this isn't necessary anywhere in the production code. This is because of the nil check in the Now() method of the Clock.

In Go (unlike other languages like Python, Java, or C#) when you call a method on a pointer to a struct, the method is invoked and the nil pointer passed in as the receiver. From there, if any references to fields on the nil struct are accessed a "nil pointer dereference" panic ensues. So, we can take advantage of this to allow our production code to be very simple, not initializing the *Clock field anywhere to ensure that time.Now() is the result in production.

Now the test code (because it belongs to the same package) can initialize that field with an actual *Clock instance that provides a tailored time.Time. We've actually created an external package with all of this behavior: clock. Here's the complete example using our clock package:

File: calendar.go (version 6 - final)
package calendar

import (
	"time"

	"github.com/smartystreets/clock"
)

type Calendar struct {
	clock *clock.Clock
}

func NewCalendar() *Calendar {
	// Notice: no constructor parameters and no initilization of the *Clock!
	return &Calendar{}
}

func (this *Calendar) CurrentQuarter() int {
	month := this.clock.UTCNow().Month()
	if month < 4 {
		return 1
	} else if month < 7 {
		return 2
	} else if month < 10 {
		return 3
	} else {
		return 4
	}
}
File: calendar_test.go
package calendar

import (
	"testing"
	"time"

	"github.com/smartystreets/clock"
)

func TestCurrentQuarter(t *testing.T) {
	cases := []struct{
		month   string
		quarter int
	}{
		{month: "01", quarter: 1},
		{month: "02", quarter: 1},
		{month: "03", quarter: 1},
		{month: "04", quarter: 2},
		{month: "05", quarter: 2},
		{month: "06", quarter: 2},
		{month: "07", quarter: 3},
		{month: "08", quarter: 3},
		{month: "09", quarter: 3},
		{month: "10", quarter: 4},
		{month: "11", quarter: 4},
		{month: "12", quarter: 4},
	}

	for _, test := range cases {
		parsed, _ := time.Parse("2006-01-02", fmt.Sprintf("2015-%s-15", test.month))
		cal := NewCalendar()
		cal.clock = clock.Freeze(parsed)
		actual := cal.CurrentQuarter()
		if actual != test.quarter {
			t.Error("Month:", test.month, "Expected Quarter:", test.quarter, "Actual Quarter:", actual)
		}
	}
}

The approach we've taken with the clock package can be utilized with any package-level resource that you want be able to replace at the struct-level. Examples:

  • time.Sleep (included with the clock package; see clock.StayAwake())
  • log.Println (see our logging package for a similar implementation)
  • os.Getenv(...)
  • flag.String
  • rand.Reader

Can you think of any other applications for this approach?

Happy testing!

Subscribe to our blog!
Learn more about RSS feeds here.
rss feed iconSubscribe Now
Read our recent posts
6 Ways Telecom & Internet Companies Leverage Address Data
Arrow Icon
In a recent webinar, we showed how address data tools help managers improve business performance in the following areas:Network PlanningAsset ManagementCustomer Acquisition and RetentionData BlendingMail and Parcel DeliveryFraud Reduction1. Network PlanningAccurate address data plays a critical role in overcoming network planning challenges. We presented solutions at each of the following steps in the network planning process:&nbsp;Clean existing address data with&nbsp;US Address VerificationKeep incoming data clean with&nbsp;US Address AutocompleteMap all addresses using our&nbsp;US Master Address List and hyper-accurate&nbsp;US Rooftop GeocodingAdd context to maps with&nbsp;enriched US Property DataAnalyze, get spatial insights, and make confident business decisions&nbsp;2.
A Fresh Start: Clean Out Your Computer Day
Arrow Icon
In a world where the lines between digital and physical spaces blur, the health of our computers has a massive impact on our productivity and overall ability to perform well at work. &nbsp;Clean Out Your Computer Day, observed on the second Monday of February, is the perfect opportunity to ensure that we declutter and optimize our virtual spaces. Just as we regularly clean our homes and offices, this day underscores the importance of maintaining a streamlined, efficient computer setup. The Digital Declutter: Where to StartStarting with the basics, sift through your files and programs.
Smarty's 2023 Address Data Deep Dive: Trends, Challenges, and Breakthroughs
Arrow Icon
We recently hosted a webinar where Brent Francom and Jeffrey Duncan, two of our Product Managers, talked about the lessons we learned and the improvements we've made in 2023. Here are the highlights. When you process so many addresses, you see things. We processed 327 billion+ addresses in 2023, equating to nearly 900 million per day or over 10,000 per second. When you process that many addresses, you find many funny and interesting ones. We keep a "Wall of Shame" with funny and malformed addresses that somehow validate.
Ready to get started?