Time Testing in Go
Testing a background job that needs to happen at a specific time repeatedly is not an easy task. It’s different from the usual deterministic approach where you input something into a function and test the expected result.
Imagine you have a function in Go that needs to run a job every Tuesday at 9 o’clock in the morning. This job could be an import, an export, data fetching, or something else.
Testing is crucial in this functionality. Without proper testing, you would have to wait for weeks to ensure it works correctly. Let’s consider a function that calculates how far we are from the next Tuesday at 9 AM.
package jobs
import "time"
var now = time.Now()
func CalculateNextTuesdayAtNine() time.Duration {
location := now.Location()
daysUntilTuesday := int(time.Tuesday - now.Weekday())
if daysUntilTuesday < 0 || (daysUntilTuesday == 0 && now.Hour() >= 9) {
daysUntilTuesday += 7
}
nextTuesdayAtNine := time.Date(now.Year(), now.Month(), now.Day()+daysUntilTuesday, 9, 0, 0, 0, location)
return nextTuesdayAtNine.Sub(now)
}
The key to testing our CalculateNextTuesdayAtNine
function is how we handle the current time, typically represented by time.Now()
. In our initial implementation, we use a global variable now
assigned to time.Now()
. However, for testing purposes, we need to refactor this approach. By abstracting how we get the current time, we can simulate different times in our tests, making it possible to thoroughly test our function under various scenarios.
Additionally, let’s see how we can use this function in a main application:
package main
import (
"fmt"
"time"
jobs "time-testing/jobs"
)
func main() {
for {
duration := jobs.CalculateNextTuesdayAtNine()
timer := time.NewTimer(duration)
<-timer.C
fmt.Println("Running task")
}
}
Testing the Functionality
Testing our function involves ensuring it accurately calculates the time until the next Tuesday at 9 AM for different dates. Here’s how the test cases are structured:
package jobs
import (
"fmt"
"testing"
"time"
)
func TestCalculateNextTuesdayAtNine(t *testing.T) {
testCase := []struct {
name string
input time.Time
expected time.Duration
}{
{
name: "First week of December",
input: time.Date(2023, time.December, 5, 8, 0, 0, 0, time.UTC),
expected: 1 * time.Hour,
},
{
name: "Christmas Day",
input: time.Date(2023, time.December, 25, 8, 0, 0, 0, time.UTC),
expected: 24*time.Hour + 1*time.Hour,
},
}
for _, tc := range testCase {
t.Run(tc.name, func(t *testing.T) {
now = tc.input
result := CalculateNextTuesdayAtNine()
fmt.Println(result)
if result != tc.expected {
t.Errorf("CalculateNextTuesdayAtNine() for %s = %v, want %v", tc.name, result, tc.expected)
}
})
}
}
Source code: time-testing