Don*_*Gar 9 testing unit-testing go
我的代码中有很多组件具有持久的go-routines,它们监听事件以触发操作.大多数情况下,他们没有理由(在测试之外)在完成该操作后发回通知.
但是,我的单元测试使用sleep来等待这些异步任务完成:
// 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{})
Run Code Online (Sandbox Code Playgroud)
这看起来很糟糕,但我还没有找到一个更好的解决方案,不会给非测试使用增加不合理的开销.我错过了合理的解决方案吗?
惯用的方法是将一个done通道和您的数据一起传递给worker go-routine.转到规应close的done频道,您的代码应该等到通道已关闭:
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{})
Run Code Online (Sandbox Code Playgroud)
使用此模式,您还可以为测试实现超时:
// Wait until `done` is closed.
select {
case <-done:
case <-time.After(10 * time.Second):
panic("timeout")
}
Run Code Online (Sandbox Code Playgroud)
Soheil Hassas Yeganeh的解决方案通常是一种很好的方式,或者至少是类似的方式.但它是一个变化的API,它可以创建一些开销调用者(虽然不多;调用方不具有传递一个Done信道,如果主叫方并不需要它).也就是说,有些情况下你不需要那种ACK系统.
我强烈推荐Gomega测试包用于解决这类问题.它的设计与Ginkgo一起使用,但可以单独使用.它通过匹配器Consistently和Eventually匹配器提供出色的异步支持.
也就是说,虽然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() // 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{}))
Run Code Online (Sandbox Code Playgroud)
显然,您可以调整迭代次数和间隔以满足您的需求.(Gomega使用1秒超时,每10ms轮询一次.)
任何实现的缺点Consistently是,无论你的超时,你必须吃每次测试运行.但实际上没有办法解决这个问题.你必须决定多长时间才能"不会发生".如果可能的话,最好转一下你的测试来检查Eventually,因为那样可以更快地成功.
Eventually有点复杂,因为你需要用它recover来捕捉恐慌,直到它成功,但它并不是太糟糕.像这样的东西:
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
}
Run Code Online (Sandbox Code Playgroud)
最终,这只是一个稍微复杂一点的版本,但它将逻辑包装成一个函数,因此它读得更好.