How I mock time in Golang
How I mock time and other static functions in Golang
The problem
In unit testing in Go, I’ve run into situations where a function depends on a calling time.Now()
, but I need to
specify the set the time as part of the test. In general, there’s three ways of solving this.
- Inject the time as a function parameter in the calling function. This works, but doesn’t help when running integration tests, as you need to keep injecting the time further and further up the stack.
- Inject the time via a time service. This is a variation of the above solution, with a
time
service passed around that includes other date methods. It suffers from the same problems. - Monkey patch the test using monkey as described here. This frankly strikes me as a bit janky, and is not guaranteed to last as a solution.
- Alias
time.Now()
using a package function var. To see this in action, read on!
Solution
Take the following program. How would we test getNow()
?
package main
import "time"
func main() {
println(getNow())
}
func getNow() string {
return time.Now().Format(time.RFC1123Z)
}
Modify it so time.Now()
is by default a function pointer to the real function, so…
package main
import "time"
var now = time.Now // Note the lack of ().
func main() {
println(getNow())
}
func getNow() string {
return now().Format(time.RFC1123Z) // Call our var on line 5 as a function.
}
That’s it.
Testing is straight forward.
package main
import (
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func Test_getNow(t *testing.T) {
now = func() time.Time { // Override the time variable with a static result, but only for this test.
return time.Date(2024, 12, 31, 15, 16, 17, 0, time.UTC)
}
actual := getNow()
assert.Equal(t, "Tue, 31 Dec 2024 15:16:17 +0000", actual)
}
During production, main.now is always initiated to the system default.
Other Uses
This also lets you mock New*()
functions provided by SDKs that without adding yet another layer.
For example, the following allows cleanly mocking out the slack.New()
SDK function without creating yet another layer.
var SlackNew = func(token string, options ...slack.Option) SlackApiInterface {
return slack.New(token, options...)
}