unit-testinggotime

How to make a mock for time.Now() in Go unit test


I have been reading various ways of making a mock for time.Now() and try to make the unit test.

Let's say we have a Customer struct.

type Customer struct {
    Name        string
    CreatedDate sql.NullTime
    Limit       float64
    Balance     float64

    Clock ClockInterface // added later
}

For example, this customer's balance will be reset every week, according to the CreatedDate.

func (c *Customer) ResetBalance() {

    // trying to not using time.Now() here to get deterministic result in unit test
    if time.Now().Weekday() == c.CreatedDate.Time.Weekday() {
        c.Balance = c.Limit
    }
}

I try to mock the time.Now() by adding a Clock interface to the struct.

func NewCustomer() *Customer {
    return &Customer{
        Clock: realClock{},
    }
}

And here is the clock.go inside the very same package.

package foo

import "time"

type ClockInterface interface {
    Now() time.Time
}

type realClock struct{}

func (r realClock) Now() time.Time {
    return time.Now()
}


// MockClock is a mock implementation of Clock for testing purposes
type MockClock struct {
    CurrentTime time.Time
}

func (m *MockClock) Now() time.Time {
    return m.CurrentTime
}

Then I can change the implementation of ResetBalance()

func (c *Customer) ResetBalance() {

    if c.Clock.Now().Weekday() == c.CreatedDate.Time.Weekday() {
        c.Balance = c.Limit
    }
}

and using a mock in the unit test.


fakeNow := time.Date(2021, 3, 7, 0, 0, 0, 0, time.UTC)
fakeClock := MockClock{CurrentTime: fakeNow}

....
....

c := &Customer{
    Clock: &fakeClock, // using fake clock from the mock
    CreatedDate: fakeDate, // various value from table tests
}

The real code is more complicated than the code above but I hope I can simplify the idea.

My question: Is there any trade off when using the additional exported field (the Clock) inside Customer or should I go using the available package out there like the Clock from benbjohnson?

Edit 1: The code implementation is huge and the actual number of methods that the Customer has is a lot. I need to test so many various test cases such as "end of the day", "leap date", end of year, weekly / monthly / yearly cycle, weekday / weekend, etc.

Edit 2: I personally can not follow the "monkey patching way" since I doubt the unit testing will have a race condition or any unexpected behavior (our legacy code is a massive amazon jungle, I don't really know what happened if I change any of the implementation). But I appreciate that alternative as a solution to mine.


Solution

  • The answers to this question (amoungst others) present a range of options for mocking time.Now() and I don't think there is much benefit in repeating them. I'm mainly answering due to the request in the comments.

    Your proposed solution appears reasonable. Making a change like this will generally have some trade off's, in this case it makes the code a little more difficuly to follow, introduces an opportunity for panic's (if Customer is created directly and you neglect to set Clock) and may have a slight performance impact. The benefits delivered via the tests probably outweigh these factors. I don't see much value in comparing your approach to other options (such as those given in answers to the question referenced above) because that would just be my opinion, and based on incomplete info (for example, I often use a global var now = time.Now, but is more limited and you may hit issues with concurrent tests).

    The difficulty testing functions that rely on times has led to the introduction of synctest. An experimental version is available in Go 1.24 behind the GOEXPERIMENT=synctest flag, but 1.25 (due this month) is expected to make this standard (with a slightly different API).

    This blog post provides a good overview of what the package offers. Basically it enables you to run a test in an isolated “bubble” where time is virtualised, this simplifies tests (especially where multiple goroutines are running).

    There are some examples in the stdlib and Anton Zhiyanov's interactive release notes also provide a good introduction. Following (playground) is one way you could test your code using this (which I think demonstrates the benefits quite nicely):

    func nextDay() time.Time {
        now := time.Now()
        yyyy, mm, dd := now.Date()
        return time.Date(yyyy, mm, dd+1, 0, 0, 0, 0, now.Location())
    }
    
    func TestResetBalance(t *testing.T) {
        synctest.Test(t, func(t *testing.T) {
            time.Sleep(time.Until(nextDay())) // Wait until start of a day (local time)
    
            var c Customer
            c.SetBalance(5)
            time.Sleep(time.Hour)
            c.ResetBalance() // day has not changed
            if c.Balance != 5 { t.Fatal("bal should be 5") }
    
            time.Sleep(24 * time.Hour) // move to next day
            c.ResetBalance()
            if c.Balance != 0 { t.Fatal("bal should be 0") }
        })
    }
    

    Note that whilst it may appear that this test would take a day to run it completes almost instantly (Playground says 0.00s). I've been using the 1.24 implementation for a while and it's led to much simpler, more readable, tests.