Golang Testing time.Now

April 1, 2019 by mtchavez

Testing time objects can be a pain. Some languages offer some helpful libraries that can help like VCR in Ruby. Other tools to help are mocking libraries to where you can mock and stub out the time calls to what you want. In golang there is another way to help and that is using dependency injection to help make testing easier.

Dependency injection is technique to cleanly manage dependencies of an object or class. Rather than having the class be responsible for creating its dependencies or delegate to another object to create them it has them injected into the object, usually through instantiation.

How this can help with testing comes along with the usage of interfaces which inform how the injected dependencies can be used. To do that we can use an interface in golang to construct a time provider

package main

import (
	"fmt"
	"time"
)

type TimeProvider interface {
	Now() time.Time
}

type testTime struct {
	TimeProvider
}

func (t *testTime) Now() time.Time {
	now, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
	return now
}

func main() {
	t := &testTime{}
	fmt.Println(t.Now())
}

The time provider interface is defining a Now() function to conform to. We will use this as a way to require any other time structs we want to declare to know what Now() means to it. For testTime we implement Now() to be 2006-01-02T15:04:05Z. Clearly this isn’t very useful but it is the basis of how we can build to a world with dependency injection of time provider structs to test our own desired setup against expected outcomes.

One step further would be adding a second struct that implements the TimeProvider and has its own concept of what Now() is.

package main

import (
	"fmt"
	"time"
)

type TimeProvider interface {
	Now() time.Time
}

type testTime struct {
	TimeProvider
}

type anotherTime struct {
	TimeProvider
}

func (t *testTime) Now() time.Time {
	now, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
	return now
}

func (t *anotherTime) Now() time.Time {
	now, _ := time.Parse(time.RFC3339, "2010-12-25T15:04:05Z")
	return now
}

func main() {
	t := &testTime{}
	fmt.Println(t.Now())

	t2 := &anotherTime{}
	fmt.Println(t2.Now())
}

Injecting A Time Provider

With our TimeProvider interface we can now make time structs for whatever usecases we have. The next step we can take from there i to use our interface as a way to inject any time structs that conform to our provider. This helps us know that we always have a Now() function to call as well as control the dependency’s definition of Now().

How that might look with a Report struct getting a time provider as a dependency injected. For our report we want to make sure the time provider injected in sets the created at of our report. As an example the provider will set Now() as the beginning of the day.

package main

import (
	"fmt"
	"time"
)

type TimeProvider interface {
	Now() time.Time
}

type Report struct {
	Name         string    `json:"name"`
	CreatedAt    time.Time `json:"created_at"`
	timeProvider TimeProvider
}

func NewReport(timeProvider TimeProvider, name string) *Report {
	return &Report{
		Name:         name,
		CreatedAt:    timeProvider.Now(),
		timeProvider: timeProvider,
	}
}

type ReportCreatedAtTime struct {
	TimeProvider
}

func (t *ReportCreatedAtTime) Now() time.Time {
	// Returns beginning of day
	now := time.Now()
	return now.Truncate(24 * time.Hour)
}

func main() {
	reportTimeProvider := &ReportCreatedAtTime{}
	report := NewReport(reportTimeProvider, "todays-report")

	fmt.Printf("Report %+v\n", report)
}

A Report is made up of the name of the report and a time it was created at. We have also added a dependency on our TimeProvider that can be passed into the NewReport function to create new reports. The usecase we wanted to satisfy is to set the CreatedAt to the beginning of today.

To achieve this the ReportCreatedAtTime implements the time provider and defines Now() to return the beginning of today. This is used to create a new report and all it has to know is it can call a Now() function on the provider it is being injected with.

The last step to this is to see how we can put this all together for a testing the report created at functionality.

Testing With The Time Provider

Now that we can inject time providers into a report the testing of our report can be pretty simple. As long as we have a provider that conforms to the TimeProvider interface we can inject our test setup with whatever conditions we want to test. Meaning we can test time in our code with little friction.

package main

import (
	"fmt"
	"testing"
	"time"
)
//
// same implementation as above but left out for brevity
//

//
// Tests
//

type testReportCreatedAtTime struct {
	TimeProvider
}

func (t *testReportCreatedAtTime) Now() time.Time {
	testTime, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
	return testTime
}

func TestNewReport(t *testing.T) {
	testCreatedAtTime := &testReportCreatedAtTime{}
	testReport := NewReport(testCreatedAtTime, "test-report")
	if testReport.CreatedAt != testCreatedAtTime.Now() {
		t.Errorf("Expected report CreatedAt to be %s but got %s", testCreatedAtTime.Now(), testReport.CreatedAt)
	}
}

In our tests we create our own test time provider testReportCreatedAtTime to use in our setup. Since we are conforming to the TimeProvider interface we need to define a Now() function to use in our test provider as well. Which we just parse an arbitrary date and time. Creating our time provider and passing it into our NewReport function for a test report allows us to know exactly what our CreatedAt time should be for our report.

We didn’t mock or stup anything out in this setup. The imports are all just standard library packages and our reporter knows nothing about time except for the fact it takes in a time provider that responds to a Now() function.

Wrapping Up

The implementation we arrived at using dependency injection to test time in our golang code is easy to follow and concise. The test setup requires no extra libraries. We aren’t mocking or stubbing out any implementation of time or the time package itself so we are using real time objects. One downside may be the amount of setup for the tests themselves could be large for interfaces that require more method signatures to implement.

Overall I have found this approach to be pretty convenient and allows for easier management of time dependencies in your codebase.


Categories

mtchavez All rights reserved.