Introduction

Recently I was working on my personal project and I encountered a situation where I was calling time.Now() to get the current system time. I was working on a function to calculate someone's age from a birthday, I needed this function to display it in my app. I did not want another field in my database with someone's age, since in my opinion this calculation from the birthday is pretty trivial.

Coding

Since the function to calculate the age is a function with a clear in and output we could easily test it using table driven tests. If you don't know what table tests this is a good explanation. The benefits of using table driven tests are that adding and adjusting test cases is fairly easy, which is exactly what I needed to test my Age function.

My implementation of the Age function looks like this.

package account import ( "time" ) type UserProfile struct { Birthday time . Time } func ( up UserProfile ) Age ( ) uint { now := time . Now ( ) currentYear := now . Year ( ) if currentYear <= up . Birthday . Year ( ) { return 0 } difference := currentYear - up . Birthday . Year ( ) if up . Birthday . Month ( ) > now . Month ( ) { return uint ( difference - 1 ) } if monthDayIsBefore ( up . Birthday , now ) { return uint ( difference - 1 ) } return uint ( difference ) } func monthDayIsBefore ( t , s time . Time ) bool { return t . Month ( ) == s . Month ( ) && t . Day ( ) > s . Day ( ) }

When I started with the tests I soon found out that I needed a way to control the time I was comparing against. For example, with the current implementation there is no way to control the now variable. This variable always depends on the system time, I found this to be bad for reproducibility of the tests.

The only assertion I could made against the return value was that the value is bigger then 0. There was no actual way to test the age, without updating the tests all the time. I needed a way to mock the now variable. The part I needed to mock was the call to time.Now() , because this one was using the implementation from the standard library which uses the system time underneath.

There are multiple ways to abstract this behaviour but I choose to go for the interface way.

I defined the following interface

package clock import ( "time" ) type Clock interface { Now ( ) time . Time }

Instead of calling time.Now() I passed this interface as a parameter to the age function and called the Now() function from the interface. Note that instead of passing it as a parameter it could also be part of the struct.

func ( up UserProfile ) Age ( c clock . Clock ) uint { now := c . Now ( ) }

Now the function itself is fully under our control, it doesnt depend on any external factors like the Now function from the time package. I made 2 implementations based on the Clock interface, one which uses the standard library time.Now() and one which uses a fixed time for in the tests.

package clock import ( "time" ) type Mock time . Time func ( m Mock ) Now ( ) time . Time { return time . Time ( m ) } type Real struct { } func ( Real ) Now ( ) time . Time { return time . Now ( ) }

In the tests we pass the Mock type with the fixed time and in the business logic we use the Real implementation.

My tests started looked like the following.

package account import ( "testing" ) func TestAge ( t * testing . T ) { testCases := [ ] struct { desc string profile UserProfile clock clock . Clock expectedAge uint } { { desc : "Test with birthday in the future" , profile : UserProfile { Birthday : time . Date ( 2011 , 1 , 1 , 1 , 1 , 1 , 1 , time . UTC ) , } , clock : clock . Mock ( time . Date ( 2010 , 1 , 1 , 1 , 1 , 1 , 1 , time . UTC ) ) , expectedAge : 0 , } , { desc : "Test with birthday in the same year but in the past" , profile : UserProfile { Birthday : time . Date ( 2010 , 1 , 1 , 1 , 1 , 1 , 1 , time . UTC ) , } , clock : clock . Mock ( time . Date ( 2010 , 2 , 1 , 1 , 1 , 1 , 1 , time . UTC ) ) , expectedAge : 0 , } , } for _ , tC := range testCases { t . Run ( tC . desc , func ( t * testing . T ) { if tC . profile . Age ( tC . clock ) != tC . expectedAge { t . Errorf ( "Expected profile age %v to be equal to expected age %v" , tC . profile . Age ( tC . clock ) , tC . expectedAge ) } } ) } }

Cleaning up the public API

Right now the dependencies are really clear and in the public exported API. For the caller of the Age function this might be a little bit to much, you might expect that just calling Age will just return the actual Age compared to the current time. Now something which is only useful for testing is leaked in the public API.

We can fix this with a private helper function.

package account type UserProfile struct { } func ( up UserProfile ) Age ( ) uint { return up . age ( clock . Real { } ) } func ( up UserProfile ) age ( c clock . Clock ) uint { }

In the tests we test the private age function, which contains the actual age calculation. The exported Age function just propagates the call to the private function with the real system time implementation.

Conclusion