Hacker News new | past | comments | ask | show | jobs | submit login
Mocking time and testing event loops in Go (dmitryfrank.com)
103 points by zdw on May 25, 2020 | hide | past | favorite | 33 comments



When I programmed Java I injected a lot of clocks and didn't find it to be that terrible. I find myself not doing that in Go, instead preferring to leave timekeeping out of as much code as possible, and letting the calling code set the time constraints.

Typically you would just do this with contexts. I was going to write down some examples of not using contexts, but I think your best bet is to just rewrite code to take contexts when it can be cancelled. A common mistake that people make is timing out some operation, usually happening in another goroutine, and then their program ends up leaking goroutines and crashing because the abandoned work was never correctly cleaned up. If your API doesn't allow you to abort, you will have to modify it, because downstream RPC servers come and go, people press the "stop" button in their browser, TCP connections close, etc.


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 get a lot of value from testing the event loop, blocking on individual messages received or not received as in the article is a reasonable way to de-flakify your tests (in that case, I'd expect the tests to inject mocks and/or use private interfaces). However, it is a code smell if any other tests are depending on the details of that event loop.


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.


Isn't this basic design for testability (TDD, if you like)? If you need some synchronization facilities to make your unit tests end cleanly, then you also need those facilities in your general APIs because applications _also_ have boundary conditions, and pretending that your application exists on an infinite timeline that neither starts nor ends is naive.


It's not about ending cleanly, it's about behaving correctly. I would say that there's nothing "basic" about testing concurrent code--it's always been hard, no matter what language you use. Exposing your concurrency guts in the API doesn't make the intrinsic complexity go away, it just shifts it somewhere else.


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 time.After(), which should in my opinion never be used in production code, as they don't support efficient cancelation. You should use timers instead.

In the end I resorted to writing my own set of interfaces.

Code: https://github.com/buildbarn/bb-storage/tree/master/pkg/cloc...

Documentation: https://pkg.go.dev/github.com/buildbarn/bb-storage/pkg/clock


>if you want to use that package in combination with gomock, you're out of luck

In general, libraries should offer structs, and you should declare your own interfaces with the subset of methods that you require.

This is somewhat messy here because the timer channel is accessed by a field rather than a method, but you could write a struct that embeds time.Timer and adds C() as a method. Then both your wrapped time.Timer and your mock/fake could fulfill your interface.


Though that is true, remember that Go doesn’t support co/contravariance on interface types.

Defining interfaces around existing types works, but quickly falls apart as soon as methods return concrete types. In those cases you need to write more complex wrappers, which is often not worth the hassle.


> The Clock interface also exposes functions like time.After(), which should in my opinion never be used in production code, as they don't support efficient cancelation.

Can you expand on this? I find time.After super useful for time arithmetic in control flow, like checking for expiry, validating leases, etc.


Keep in mind that there are two functions called After(). One is a method of time.Time and can be used to compare timestamps. That’s the one you are likely talking about, and is perfectly fine to use!

I’m talking about the free function After() that’s part of the time package:

https://golang.org/pkg/time/#After

That one can be used to create a channel that triggers after a certain amount of time. The reason you shouldn’t be using that one is because you can’t clean it up before it triggers. This means that any code that bails out early ends up leaking memory temporarily.


Ah, gotcha -- I was thinking of the timestamp comparison method. (Agreed about the other case.)


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.


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().


I've maintained a few codebases that relied directly on stdlib time in ways that were hard to write tests for. Sometimes the code works reliably, but tests are flaky, due to the author using time.Sleep in tests, or something else to paper over hard-to-test functions.

I'd say in a lot of, or most circumstances it's best to just bite the bullet and refactor the mess into something more maintainable.

But there are exceptions to every rule, and let's face it, sometimes it's just too expensive.

Well what you can often do in these circumstances is just mock out the whole damn time package. That's what I did when faced with a similar situation and I was pleasantly surprised with the results.

I wrote https://github.com/echlebek/timeproxy to be a drop-in import for stdlib time. (Modulo any drift in stdlib since it was written)

I wrote https://github.com/echlebek/crock as a fake time implementation that I can exercise complete control over. It lets me control tickers, timers, time.Now, and all that however I like.

Since timeproxy just dispatches all calls to stdlib by default, I feel confident using it in production.

crock has some bugs, but they usually only show up in pathological cases, and you can easily prove that its code path will only get executed in tests, by ensuring that it only gets imported in _test.go files.

So far to my knowledge I'm the only user of these libraries, although I have employed them in a large open source application. Really, the libraries are less important than concept, which I learned from a Go author's slide deck on mocking out the os library years ago.


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.


I am using java.time.Clock class in Java - really similar to what’s mentioned for Go in the article! It works great and enables us to write reliable, comprehensive, reproduce-able unit tests.


Same here. Any code that references the current time gets it from a Clock parameter.


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.


Makes me wonder how simulating time could work? Are there any good reads on how you'd model an environment where variable time intervals are critical factors in the simulation?


There is nothing deep going on here. People inject fake clocks so they can write a test like assertRow(got, row{creationTime: time.Date(2020, 5, 25, 0, 0, 0, 0)}) and have the test always pass. (Or cases like "on Sunday, do X" and you want your unit tests to work on Sunday as well as other days.) Additionally, you sometimes want shorter timeouts against your fake backend, so that when you make an error it takes milliseconds for your test to fail instead of the longer amount of time you'd prefer in production.

You don't have to use globals for any of this, but people like doing it, and then have a bunch of workarounds to make the tests work. If you don't want workarounds, your API can be "func CreateRow(..., now time.Time)" or "func DoSomethingWithTimeout(timeout time.Duration)" and just pass the correct values in. (Don't do the timeout one, though, just use a context.)


An interesting use case I saw was on a complex VoIP system with lots of internal message passing, following an actor model. There were tens of thousands of unit tests to assert all kinds of crazy interop scenarios, and the system clock was mocked out globally.

So you could set up multiple incoming streams, advance the clock to a very high level of precision and assert e.g packet A triggered packet B after 20ms, or timer X fired after 1s. This made the tests extremely reliable and fast to run, since you were pretty much CPU bound - something that I don't think would have been otherwise possible.


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?


Most people probably won't need to figure that out, and if they did then that would count as the "something fancy" I mentioned.

I'm not jumping to conclusions, I have my own mock for time that I wrote in an afternoon which has survived several years on a big golang app


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.


Go doesn't differentiate the program's monotonic time from systemtime in terms of the package interface given.

time.Now in go returns both the system time (i.e. if you do '.Seconds' or 'String' on that time, you get a wall clock time), but also returns a monotonic time (if you subtract or compare two times, it uses monotonic time).

Go's time package isn't just systemtime, and there's really not an alternative to it in the language. You're arguing against a strawman.


I don't think you got my point though. If you're using timestamps for anything else than dumping into a logfile (or purely for displaying/cosmetic purposes), you're doing it wrong. Somewhere along the line, you could end up with a nasty surprise. Not thinking of clock drift or any kind of seasonal adjustments here.

Maybe you're using time-package right and avoiding the above, maybe not. Timestamp usage is best to avoid for anything business critical.


We're misunderstanding each other then.

I assumed this package about mocking time was _not_ about mocking timestamps but program behavior. For example, if I have a loop that says "every 20 seconds, do X", and I want to test that, I now have to mock time.

That's not about timestamps, that's about comparing monotonic clocks for "20s have elapsed".

The go time package doesn't just give you a timestamp to do something with, it also lets you compare times. That was my point.

It sounds like your original comment was arguing that mocking time isn't useful, so I assumed you meant all uses of time.


Fair enough. If you want to mock time differentials like that, I see that one could use something to mock that. However, what you probably ideally would want to mock are the order of events, and not certain language-specific stdlib-calls. Though would depend how explicit you want to define your domain.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: