Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing for asynchronous results without sleep in Go

I have quite a few components in my code that have persistent go-routines that listen for events to trigger actions. Most of the time, there is no reason (outside of testing) for them to send back a notification when they have completed that action.

However, my unittests are using 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{})

That seems broken, but I haven't been able to come up with a better solution that doesn't add unreasonable overhead to non-test usage. Is there a reasonable solution that I've missed?

like image 892
DonGar Avatar asked May 24 '15 18:05

DonGar


People also ask

What is the difference between async ()' and fakeAsync?

In almost all cases, they can be used interchangeably, but using fakeAsync()/tick() combo is preferred unless you need to make an XHR call, in which case you MUST use async()/whenStable() combo, as fakeAsync() does not support XHR calls. For the most part they can be used interchangeably.

How do you test concurrency in go?

If you want to test concurrency you'll need to make a few go routines and have them run, well, concurrently. You might try taking the locking out and intentionally get it to crash or misbehave by concurrently modifying things and then add the locking in to ensure it solves it.

Does Jasmine support asynchronous operations?

Jasmine supports three ways of managing asynchronous work: async / await , promises, and callbacks.


2 Answers

The idiomatic way is to pass a done channel along with your data to the worker go-routine. The go-routine should close the done channel 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 pattern, 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")
}
like image 154
Soheil Hassas Yeganeh Avatar answered Sep 19 '22 12:09

Soheil Hassas Yeganeh


Soheil Hassas Yeganeh's solution is usually a good way to go, or at least something like it. But it is a change to the API, and it can create some overhead for the caller (though not much; the caller doesn't have to pass a Done channel if the caller doesn't need it). That said, there are cases where you don't want that kind of ACK system.

I highly recommend the testing package Gomega for that kind of problem. It's designed to work with Ginkgo, but can be used standalone. It includes excellent async support via the Consistently and Eventually matchers.

That said, while Gomega works well with non-BDD test systems (and integrates fine into testing), it is a pretty big thing and can be a commitment. If you just want that one piece, you can write your own version of these assertions. I recommend following Gomega's approach though, which is polling rather than just a single sleep (this still sleeps; it isn't possible to fix that without redesigning your API).

Here's how to watch for things in testing. You create a helper function like:

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 tweak iterations and interval to match your needs. (Gomega uses a 1 second timeout, polling every 10ms.)

The downside of any implementation of Consistently is that whatever your timeout, you have to eat that every test run. But there's really no way around that. You have to decide how long is long enough to "not happen." When possible, it's nice to turn your test around to check for Eventually, since that can succeed faster.

Eventually is a little more complicated, since you'll need to use recover to catch the panics until it succeeds, but it's not too bad. Something like this:

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 complicated version of what you have, but it wraps the logic up into a function so it reads a bit better.

like image 25
Rob Napier Avatar answered Sep 20 '22 12:09

Rob Napier