Live data from Hacker News

Mocking time and testing event loops in Go

dmitryfrank.com

21–30 of 35 posts

Re: Mocking time and testing event loops in Go

#21
post #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 synchronizat…

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.

Re: Mocking time and testing event loops in Go

#22
post #15

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

Re: Mocking time and testing event loops in Go

#23

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…

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

Re: Mocking time and testing event loops in Go

#24
post #23

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…

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

Re: Mocking time and testing event loops in Go

#25

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…

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

Re: Mocking time and testing event loops in Go

#26
post #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.

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.

Re: Mocking time and testing event loops in Go

#27
post #23

Earlier quoted context omitted.

> 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 shoul…

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

Re: Mocking time and testing event loops in Go

#28
post #21
post #12

Earlier quoted context omitted.

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 synchronizat…

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.

Re: Mocking time and testing event loops in Go

#29
post #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?

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

Re: Mocking time and testing event loops in Go

#30

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…

>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.Tim…

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.

Post reply on HN