Live data from Hacker News

Mocking time and testing event loops in Go

dmitryfrank.com

11–20 of 35 posts

Re: Mocking time and testing event loops in Go

#11

I initially played with passing an instance of a mockable time thing around everywhere that needed time, but it felt over the top to be passing around so many things, solely for purposes of testing. So now it's just a thing you mock at a global level, e.g. defer dates.SetNowSource(dates.DefaultNowSource) dates.SetNowSource(dates.NewSequentialNowSource(time.Date(2018, 10, 18, 14, 20, 30, 123456, time.UTC))) dates.Now(…

I've done this too, because passing a mockable time everywhere would just clutter the code, and this is an instance where a package-level global isn't going to bite you in the ass -- in prod anyway (in tests you might get your wires crossed, but seen another way, a global ensures all parts of your program think it's the same time).

Difference in mine is that dates.SetNowSource returns a reset function that you can defer, like with context.WithCancel().

Re: Mocking time and testing event loops in Go

#12

In my experience so far, everything beyond mocking the current time is out on the long tail of tests that are expensive to write and provide little value. When I've run into a class with a time-based event loop, I isolate the timing code as well as is reasonable and just test everything else. Or, create the timer outside and inject its channel. Want to fire the timer? Just write to the channel. If you do expect to ge…

It can be useful to directly unit test edge cases in critical concurrent code, because they are otherwise difficult to test deterministically. But like you said, I've also found them difficult to write and maintain (re-reading some of them months later is usually hard). The tests usually end up with 2-3x channels than the prod code, because I'm forced to inject channels into various places to control the synchronization. Sometimes though, in a critical code path, it's worth it.

Re: Mocking time and testing event loops in Go

#13
There are two ways I've done this.

1. Pass in a value called `now func() time.Time` into the constructor function of my struct. This allows me to call `inst.now()` on a particular instance of my structure.

2. Have a global field in a package: `var Now func() time.Time = time.Now` This allows `mypkg.Now()`

All of this avoids taking yet another external package dependency with only a couple of lines of code.

Re: Mocking time and testing event loops in Go

#14

There are two ways I've done this. 1. Pass in a value called `now func() time.Time` into the constructor function of my struct. This allows me to call `inst.now()` on a particular instance of my structure. 2. Have a global field in a package: `var Now func() time.Time = time.Now` This allows `mypkg.Now()` All of this avoids taking yet another external package dependency with only a couple of lines of code.

The yet another external dependency is way more than Now() though. Mocking just Now() is trivial, but most of the time it's by far not the only function which I personally use from the time package.

Re: Mocking time and testing event loops in Go

#16

There are two ways I've done this. 1. Pass in a value called `now func() time.Time` into the constructor function of my struct. This allows me to call `inst.now()` on a particular instance of my structure. 2. Have a global field in a package: `var Now func() time.Time = time.Now` This allows `mypkg.Now()` All of this avoids taking yet another external package dependency with only a couple of lines of code.

The yet another external dependency is way more than Now() though. Mocking just Now() is trivial, but most of the time it's by far not the only function which I personally use from the time package.

Correct. In my use case, I just need a simple replacement for time.Now(). If I needed more (time.After, for example), there's a point that bringing in an external dependency makes sense.

Re: Mocking time and testing event loops in Go

#17
post #7

I also started using benbjohnson/clock about a year ago, but discovered that it wasn't a perfect fit: - Even though Clock is an interface, Timer is not. This means that if you want to use that package in combination with gomock, you're out of luck. - The Clock interface doesn't provide a wrapper for context.WithTimeout(), which also depends on the system clock. - Nit: The Clock interface also exposes functions like t…

Not a mock obviously but in the interest of getting things done on schedule I’d suggest running tests in a container with https://github.com/wolfcw/libfaketime and orchestrating edge cases like leap seconds external to the test suite itself.

Interestingly, libfaketime probably won't work in go due to their allergy to using libc on linux. I believe they only use syscalls directly for time-related functionality, meaning that any LD_PRELOAD hack won't work.

Re: Mocking time and testing event loops in Go

#18
If you're using systemtime for anything else than dumping into logs as timestamps, you're doing it wrong.

Mocking system clocks for testing just confirms that truth. If you can't think of an alternative to using some variation of systemtime function for business operational rules, I'm not really sure what you're pushing to production.

Re: Mocking time and testing event loops in Go

#19
I recently ran into a situation where I had to test some complex scheduling logic for edge case handling (DST, leap year, new year, etc). Most of the actual time comparisons were offloaded to a database engine, so I was able to mock various scenarios with libfaketime. It was definitely a pain to not be able to mock time in Go using libfaketime though. It's a really rare requirement, but when it does pop up, it's hard to do without.

Re: Mocking time and testing event loops in Go

#20
post #9

Mocking the time module is not very hard, and I recommend writing your own custom mock that fits exactly what you need unless one of the available libraries does something particularly fancy that you need

It's surprisingly tricky, I wouldn't jump to conclusions. It would depend a great deal on how much fidelity you require to Go's undocumented behavior. For example did you know that the Go runtime (as of today) will not initiate any AfterFunc calls until the goroutine that called AfterFunc yields? Is your application silently reliant on this undocumented synchronization?
Post reply on HN