在Go中测试不睡眠的异步结果

时间:2015-05-24 18:41:26

标签: unit-testing 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{})

这似乎已经破了,但是我无法想出一个更好的解决方案,不会给非测试使用增加不合理的开销。我错过了一个合理的解决方案吗?

2 个答案:

答案 0 :(得分:7)

惯用法是将done频道与您的数据一起传递给工作人员。常规例程应该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{})

使用此模式,您还可以为测试实现超时:

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

答案 1 :(得分:5)

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() // 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{}))

显然,您可以调整迭代次数和间隔以满足您的需求。 (Gomega使用1秒超时,每10ms轮询一次。)

Consistently的任何实现的缺点是,无论你的超时,你都必须吃掉每次测试。但是,真的没办法解决这个问题。你必须决定多长时间才能发生。#34;如果可能的话,很高兴将您的测试转向检查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
}

最终,这只是一个稍微复杂一点的版本,但它将逻辑包装成一个函数,因此它读得更好。