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

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)

这看起来很糟糕,但我还没有找到一个更好的解决方案,不会给非测试使用增加不合理的开销.我错过了合理的解决方案吗?

Soh*_*neh 8

惯用的方法是将一个done通道和您的数据一起传递给worker go-routine.转到规应closedone频道,您的代码应该等到通道已关闭:

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)


Rob*_*ier 6

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

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

  • testify 通过 `assert.Eventually(...)` 支持此功能:https://pkg.go.dev/github.com/stretchr/testify/assert#Eventually (2认同)