【问题标题】:How can I rewrite this select statement for guaranteed 100% test coverage?如何重写此选择语句以保证 100% 的测试覆盖率?
【发布时间】:2016-12-11 01:00:45
【问题描述】:

这让我发疯了。假设我有以下功能:

func Map(quit <-chan struct{}, dst chan<- interface{}, src <-chan interface{}, f func(interface{}) interface{} {
    for {
        select {
        case v, ok := <- src:
            if !ok {
                return
            }
            select {
            case dst <- f(v):
            case <-quit:
                return
            }
        case <-quit:
            return
        }
    }
}

对于从 src 接收到的每个值 v,它在 dst 上发送 f(v),直到 src 或 quit 关闭并为空或从 quit 接收到值。

现在,假设我想编写一个测试来证明它可以被取消:

func TestMapCancel(t *testing.T) {
    var wg sync.WaitGroup

    quit := make(chan struct{})
    success := make(chan struct{})
    wg.Add(3)

    src := // channel providing arbitrary values until quit is closed
    dst := make(chan interface{})

    // mapper
    go func() {
        defer wg.Done()
        defer close(dst)
        Map(quit, dst, src, double)
    }()

    // provide a sink to consume values from dst until quit is closed
    timeout(quit, 10*time.Millisecond)
    wait(success, &wg)

    select {
    case <-success:
    case <-time.After(100 * time.Millisecond):
        t.Error("cancellation timed out")
    }
}

这里的未定义函数并不是非常重要。请假设它们有效。 timeout 在指定时间后关闭其通道参数,waitwg.Wait() 后关闭其通道参数。

问题在于,这不会提供 100% 的覆盖率,因为如果两者都准备好发送/接收,则会以(伪)随机方式统一选择一个选择案例。以下版本的Map 没有此问题,但如果上游通道(src)未关闭,则可能会出现无限期阻塞:

func Map(quit <-chan struct{}, dst chan<- interface{}, src <-chan interface{}, f func(interface{}) interface{}) {
    for v := range src {
        select {
        case dst <- f(v):
        case <-quit:
            return
        }
    }
}

我可以通过重写测试以在循环中重复几次来解决这个问题,这样每个分支都有机会被随机选择。我尝试了少至 10 次迭代,并在所有测试通过的情况下达到了 100% 的覆盖率(除此之外还有其他测试)。但它让我大吃一惊,我似乎无法编写一个两全其美的版本,如果上游通道没有关闭,它不会阻塞,并且保证提供 100% 的测试覆盖率(不仅仅是可能)。

对我有什么启发吗?

P.S.,如果您很好奇为什么“如果上游通道未关闭则不阻塞”很重要,这只是强迫症的另一种表现。该函数被导出,这意味着如果客户端代码行为不端,我的代码行为不端。我希望它比第一个版本更有弹性。

【问题讨论】:

    标签: testing go code-coverage channel


    【解决方案1】:

    编辑:我大量编辑了这个答案,因为它仍然不正确。这似乎工作得很好。

    好的,所以我觉得很害羞。当我看到这个的时候,我一定已经筋疲力尽了。绝对有可能以确定的方式遍历这些 select 语句:

    func TestMapCancel(t *testing.T) {
        src := make(chan interface{})
        quit := make(chan struct{})
        done := make(chan struct{})
    
        go func() {
            defer close(done)
            Map(quit, nil, src, double)
        }()
    
        close(quit)
    
        select {
        case <-done:
        case <-time.After(100 * time.Millisecond):
            t.Error("quitting pre-send failed")
        }
    
        src = make(chan interface{})
        quit = make(chan struct{})
        done = make(chan struct{})
    
        go func() {
            defer close(done)
            Map(quit, nil, src, double)
        }()
    
        src <- 1
        close(quit)
    
        select {
        case <-done:
        case <-time.After(100 * time.Millisecond):
            t.Error("quitting pre-send failed")
        }
    }
    

    【讨论】:

    • 我会把它分成多个测试函数。此外,还有两个您没有测试的退出案例:从src 接收然后在dst 上发送然后 关闭quit,并退出因为src 已关闭。 play.golang.org/p/KI9OJLsHdc
    • 那些已经在其他测试函数中处理了。只是这个特别的让我感到不适,因为我没有意识到我可以通过将通道设置为 nil 来确定地遍历 select 语句,这样他们就永远不会收到。
    • 老实说,甚至不需要将它们设置为 nil。简单地使它们成为您不调用接收的无缓冲通道在机械上与通道为 nil 相同。如果您创建了一个无缓冲的dst 通道并将其交给Map,那么您的测试根本不会改变,并且从不费心去做&lt;-dst
    • 嗯,在这个版本之前,我是在dst频道下沉的(在它下面的select stmt中)。这也是一个问题。我猜,添加 done 频道确实是它的功劳。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-06
    • 2023-03-21
    • 1970-01-01
    相关资源
    最近更新 更多