在Go中进行无需休眠的异步结果测试

huangapple go评论74阅读模式
英文:

Testing for asynchronous results without sleep in Go

问题

我在我的代码中有很多组件,它们有持久的go-routines来监听事件并触发操作。大部分时间,除了测试之外,它们完成操作后不需要发送通知。

然而,我的单元测试中使用了sleep来等待这些异步任务完成:

// 发送通知事件。
mock.devices <- []sparkapi.Device{deviceA, deviceFuncs, deviceRefresh}

// 等待go-routine处理事件。
time.Sleep(time.Microsecond)
    
// 检查是否调用了刷新方法。
c.Check(mock.actionArgs, check.DeepEquals, mockFunctionCall{})

这似乎有问题,但我还没有找到一个更好的解决方案,它不会给非测试使用带来不合理的开销。有没有我忽视的合理解决方案?

英文:

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?

答案1

得分: 13

惯用的方法是将一个done通道与你的数据一起传递给工作协程。协程应该close这个done通道,你的代码应该等待通道关闭:

done := make(chan bool)

// 发送通知事件。
mock.devices <- Job {
    Data: []sparkapi.Device{deviceA, deviceFuncs, deviceRefresh},
    Done: done,
}

// 等待`done`通道关闭。
<-done

// 检查是否没有调用刷新方法。
c.Check(mock.actionArgs, check.DeepEquals, mockFunctionCall{})

使用这种模式,你还可以为你的测试实现超时:

// 等待`done`通道关闭。
select {
case <-done:
case <-time.After(10 * time.Second):
    panic("timeout")
}
英文:

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 &lt;- Job {
    Data: []sparkapi.Device{deviceA, deviceFuncs, deviceRefresh},
    Done: done,
}

// Wait until `done` is closed.
&lt;-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 &lt;-done:
case &lt;-time.After(10 * time.Second):
    panic(&quot;timeout&quot;)
}

答案2

得分: 11

Soheil Hassas Yeganeh的解决方案通常是一个不错的选择,或者至少类似的解决方案。但这是对API的更改,并且可能会给调用者带来一些额外的开销(尽管不多;如果调用者不需要,调用者不需要传递Done通道)。也就是说,有些情况下,您不希望使用这种类型的ACK系统。

我强烈推荐使用Gomega测试包来解决这种问题。它专为Ginkgo设计,但也可以独立使用。它通过ConsistentlyEventually匹配器提供了出色的异步支持。

尽管Gomega可以很好地与非BDD测试系统配合使用(并且可以很好地集成到testing中),但它是一个相当庞大的东西,并且可能需要一些承诺。如果您只想要其中的一部分,可以编写自己版本的这些断言。不过,我建议遵循Gomega的方法,即使用轮询而不仅仅是单个休眠(这仍然会休眠;如果不重新设计API,无法解决这个问题)。

以下是如何在测试中监视事物的方法。您可以创建一个类似下面的辅助函数:

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

const iterations = 10
const interval = time.Millisecond

func Consistently(f func()) {
    for i := 0; i < iterations; i++ {
        f() // 假设这里的`f()`在失败时会引发panic
        time.Sleep(interval)
    }
}

mock.devices <- []sparkapi.Device{deviceA, deviceFuncs, deviceRefresh}
Consistently(c.Check(mock.actionArgs, check.DeepEquals, mockFunctionCall{}))

显然,您可以根据需要调整iterations和interval。 (Gomega使用1秒的超时时间,每10毫秒轮询一次。)

Consistently的任何实现的缺点是,无论超时时间如何,您都必须在每次测试运行时都等待。但实际上没有办法避免这一点。您必须决定多长时间足够“不会发生”。如果可能的话,将测试转换为检查Eventually会更好,因为这样可以更快地成功。

Eventually要复杂一些,因为您需要使用recover来捕获panic,直到它成功为止,但情况并不糟糕。类似于以下代码:

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
}

总的来说,这只是稍微复杂一些的版本,但它将逻辑封装到一个函数中,使得代码阅读起来更清晰。

英文:

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 &lt; iterations; i++ {
		f() // Assuming here that `f()` panics on failure
		time.Sleep(interval)
	}
}

mock.devices &lt;- []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 &lt; iterations; i++ {
		if !panics(f) {
			return
		}
		time.Sleep(interval)
	}
	panic(&quot;FAILED&quot;)
}

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.

huangapple
  • 本文由 发表于 2015年5月25日 02:41:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/30427013.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定