[Go] How to work with dates in tests

Elton Minetto - Jul 3 '23 - - Dev Community

Working with dates in any programming language presents some challenges. In this post, I will show how to work with dates when writing unit tests for a Go application.

Let's go to the example:

import (
    "time"

    "github.com/google/uuid"
)

type Food struct {
    ID             uuid.UUID
    Name           string
    ExpirationDate time.Time
}

func canIEat(f Food) bool {
    if time.Now().Before(f.ExpirationDate) {
        return true
    }
    return false
}
Enter fullscreen mode Exit fullscreen mode

One way to write a unit test for this code could be:

import (
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
)

func TestCanIEat(t *testing.T) {
    f1 := Food{
        ExpirationDate: time.Now().AddDate(0, 0, 1),
    }
    assert.True(t, canIEat(f1))

    f2 := Food{
        ExpirationDate: time.Now().AddDate(0, 0, -1),
    }
    assert.False(t, canIEat(f2))
}
Enter fullscreen mode Exit fullscreen mode

Another way to solve this is to create an abstraction for the time package. For this, we will create a new package called clock, inside it, we will add the clock.go file:

package clock

import "time"

// Clock interface
type Clock interface {
    Now() time.Time
}

// RealClock clock
type RealClock struct{}

// NewRealClock create a new real clock
func NewRealClock() *RealClock {
    return &RealClock{}
}

// Now returns the current data
func (c *RealClock) Now() time.Time {
    return time.Now()
}
Enter fullscreen mode Exit fullscreen mode

The next step is to refactor the function that will use the new package:

import (
    "time"

    "github.com/eminetto/post-time/clock"
    "github.com/google/uuid"
)

type Food struct {
    ID             uuid.UUID
    Name           string
    ExpirationDate time.Time
}

func canIEat(c clock.Clock, f Food) bool {
    if c.Now().Before(f.ExpirationDate) {
        return true
    }
    return false
}
Enter fullscreen mode Exit fullscreen mode

As the canIEat function receives the clock.Clock interface, we can, in our test, use a new implementation of this interface:

import (
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
)

type FakeClock struct{}

func (c FakeClock) Now() time.Time {
    return time.Date(2023, 6, 30, 20, 0, 0, 0, time.Local)
}

func TestCanIEat(t *testing.T) {
    type test struct {
        food     Food
        expected bool
    }
    fc := FakeClock{}
    cases := []test{
        {
            food: Food{
                ExpirationDate: time.Date(2020, 10, 1, 20, 0, 0, 0, time.Local),
            },
            expected: false,
        },
        {
            food: Food{
                ExpirationDate: time.Date(2023, 6, 30, 20, 0, 0, 0, time.Local),
            },
            expected: false,
        },
        {
            food: Food{
                ExpirationDate: time.Date(2023, 6, 30, 21, 0, 0, 0, time.Local),
            },
            expected: true,
        },
    }
    for _, c := range cases {
        assert.Equal(t, c.expected, canIEat(fc, c.food))
    }
}

Enter fullscreen mode Exit fullscreen mode

This way, we better control what will we are using in the test and gain performance because it is no longer necessary to do date calculations like the time.Now().AddDate(0, 0, 1) of the first example.

What I'm doing here is a simple tip, but it shows how powerful and easy to use the concept of interfaces in Go is.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .