Testing asynchronous sleep-free results in Go

I have quite a few components in my code that have constant running procedures that listen for events to trigger actions. In most cases, there is no reason (outside of testing) for them to send a notification when they complete this action.

However, my unittests use sleep to wait for these async tasks to complete:

// Send notification event. mock.devices <- []sparkapi.Device{deviceA, deviceFuncs, deviceRefresh} // Wait for go-routine to process event. time.Sleep(time.Microsecond) // Check that no refresh method was called. c.Check(mock.actionArgs, check.DeepEquals, mockFunctionCall{}) 

It seems broken, but I could not come up with a better solution that does not add unreasonable overhead to use without testing. Is there any reasonable solution I missed?

+7
unit-testing go testing
source share
2 answers

Soheil Hassas Yeganeh's solution is usually a good way, or at least something like that. But this is an API change, and it can create some overhead for the caller (although not so much: the caller does not need to transmit the Done channel if he does not need the caller). However, there are times when you do not need such an ACK system.

I highly recommend the Gomega test suite for this kind of problem. It is designed to work with Ginkgo , but can be used autonomously. It includes excellent async support using Consistently and Eventually .

However, while Gomega works well with testing systems other than BDD (and integrates perfectly with testing ), this is a pretty big thing and can be a commitment. If you just want one part, you can write your own version of these statements. I recommend using the Gomega approach, although this is a survey, not just a dream (it is still sleeping, it is impossible to fix it without remodeling your API).

Here's how to look at things in testing. You create a helper function, for example:

http://play.golang.org/p/qpdEOsWYh0

 const iterations = 10 const interval = time.Millisecond func Consistently(f func()) { for i := 0; i < iterations; i++ { f() // Assuming here that `f()` panics on failure time.Sleep(interval) } } mock.devices <- []sparkapi.Device{deviceA, deviceFuncs, deviceRefresh} Consistently(c.Check(mock.actionArgs, check.DeepEquals, mockFunctionCall{})) 

Obviously, you can customize the iteration and spacing to suit your needs. (Gomega uses a timeout of 1 second, polling every 10 ms.)

The disadvantage of any Consistently implementation is that regardless of your timeout, you should eat that every test run. But in fact this does not happen. You have to decide how much time is enough to not happen. Whenever possible, it's nice to test your test on Eventually , as it can speed up.

Eventually little more complicated, since you need to use recover to catch the panic until it succeeds, but it’s not so bad. Something like that:

 func Eventually(f func()) { for i := 0; i < iterations; i++ { if !panics(f) { return } time.Sleep(interval) } panic("FAILED") } func panics(f func()) (success bool) { defer func() { if e := recover(); e != nil { success = true } }() f() return } 

Ultimately, this is just a slightly more complex version of what you have, but it turns logic into a function so that it reads a little better.

+4
source share

The idiomatic way is to pass the done channel along with your data to the work program. The routine should close channel done , and your code should wait until the channel is closed:

 done := make(chan bool) // Send notification event. mock.devices <- Job { Data: []sparkapi.Device{deviceA, deviceFuncs, deviceRefresh}, Done: done, } // Wait until `done` is closed. <-done // Check that no refresh method was called. c.Check(mock.actionArgs, check.DeepEquals, mockFunctionCall{}) 

Using this template, you can also implement a timeout for your test:

 // Wait until `done` is closed. select { case <-done: case <-time.After(10 * time.Second): panic("timeout") } 
+6
source share

All Articles