Meu nome é Elton Minetto

[Go] Como trabalhar com datas em testes

Trabalhar com datas em qualquer linguagem de programação trás alguns desafios interessantes. Neste post vou mostrar uma forma de trabalhar com datas ao escrever testes unitários para uma aplicação em Go.

Vamos ao exemplo:

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
}

Uma forma de escrevermos um teste unitário para este código pode ser:

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))
}

Outra forma de se resolver isso é criarmos uma abstração para o pacote time. Para isso vamos criar um novo pacote, chamado clock e dentro dele criamos o arquivo clock.go:

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()
}

O próximo passo é refatorar a função que vai usar o novo pacote:

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
}

Como a função canIEat recebe a interface clock.Clock podemos, no nosso teste, enviar uma nova implementação desta 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))
	}
}

Desta forma temos um controle melhor do que será usado no teste, além de ganharmos performance pois não é mais preciso fazer cálculos de data como o time.Now().AddDate(0, 0, 1) do primeiro exemplo.

Essa é uma dica simples mas que mostra como é poderoso e fácil de usar o conceito de interfaces em Go.