如何在单元测试中检查 sync.WaitGroup.Done() 是否被调用?

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

How to check if sync.WaitGroup.Done() is called in unit test

问题

假设我有一个异步执行的go例程函数f

func f(wg *sync.WaitGroup){
    defer wg.Done()
    // Do sth
}

func main(){
    var wg sync.WaitGroup
    wg.Add(1)
    go f(&wg)
    wg.Wait() // 等待f执行完毕
    // ...
}

我该如何为函数f创建一个单元测试,以确保wg.Done()被调用?

一种选择是在测试中直接在调用f之后调用wg.Done()。如果f未调用wg.Done(),测试将会引发panic,这并不好。
另一种选择是为sync.WaitGroup创建一个接口,但这似乎有点奇怪。

英文:

Let's say I have a function if that is executed asynchronous as a go routine:

func f(wg *sync.WaitGroup){
    defer wg.Done()
    // Do sth
}

main(){
    var wg sync.WaitGroup
    wg.Add(1)
    go f(&wg)
    wg.Wait() // Wait until f is done
    // ...
}

How would I create a unit test for f that makes sure wg.Done() is called?

One option is to call wg.Done() in the test directly after the f is called. If f fails to call wg.Done() the test will panic which is not nice.
Another option would be to create an interface for sync.WaitGroup but that seems a bit weird.

答案1

得分: 1

如何为函数f创建一个单元测试,以确保wg.Done()被调用?

可以像这样编写一个单元测试:

func TestF(t *testing.T) {
    wg := &sync.WaitGroup{}
    wg.Add(1)

    // 异步运行任务
    go f(wg)

    // 等待WaitGroup完成,或者超时
    select {
    case <-wrapWait(wg):
        // 一切正常
    case <-time.NewTimer(500 * time.Millisecond).C:
        t.Fail()
    }
}

// 辅助函数,允许在select中使用WaitGroup
func wrapWait(wg *sync.WaitGroup) <-chan struct{} {
    out := make(chan struct{})
    go func() {
        wg.Wait()
        out <- struct{}{}
    }()
    return out
}

在这个测试中,你不直接检查WaitGroup,因为你无法直接检查它。相反,你断言函数的行为符合预期,给定预期的输入。

在这种情况下,预期的输入是WaitGroup参数,预期的行为是wg.Done()最终会被调用。实际上,这意味着如果函数成功,计数为1的WaitGroup将变为0,并允许wg.Wait()继续执行。

f函数开头的defer wg.Done()语句已经确保测试对错误或崩溃具有弹性。添加超时只是为了确保测试在合理的时间内完成,即它不会使测试套件长时间停滞。个人而言,我更喜欢使用显式的超时,无论是使用定时器还是上下文,原因是:1)避免在CI级别忘记设置超时时出现问题,2)使时间上限对任何检出仓库并运行测试套件的人都可用,即避免依赖于IDE配置或其他设置。

英文:

> How would I create a unit test for f that makes sure wg.Done() is called?

Something like this:

func TestF(t *testing.T) {
    wg := &amp;sync.WaitGroup{}
    wg.Add(1)

    // run the task asynchronously
    go f(wg)

    // wait for the WaitGroup to be done, or timeout
    select {
    case &lt;-wrapWait(wg):
        // all good
    case &lt;-time.NewTimer(500 * time.Millisecond).C:
        t.Fail()
    }
}

// helper function to allow using WaitGroup in a select
func wrapWait(wg *sync.WaitGroup) &lt;-chan struct{} {
    out := make(chan struct{})
    go func() {
        wg.Wait()
        out &lt;- struct{}{}
    }()
    return out
}

You don't inspect the WaitGroup directly, which you can't do anyway. Instead you assert that the function behaves as expected, given the expected input.

In this case, the expected input is the WaitGroup argument and the expected behavior is that wg.Done() gets called eventually. What does that mean, in practice? It means that if the function is successful a WaitGroup with count 1 will reach 0 and allow wg.Wait() to proceed.

The statement defer wg.Done() at the beginning of f already makes sure that the test is resilient to errors or crashes. The addition of a timeout is simply to make sure the test will complete within a reasonable time, i.e. that it doesn't stall your test suite for too long. Personally, I prefer using explicit timeouts, either with timers or with contexts, to 1) avoid problems if someone forgets to set timeouts at the CI level, 2) make the time ceiling available to anyone who checks out the repo and runs the test suite, i.e. avoid dependencies on IDE configs or whatnot.

huangapple
  • 本文由 发表于 2021年11月20日 22:02:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/70046636.html
匿名

发表评论

匿名网友

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

确定