如何在Golang中使用并行子测试处理父级测试拆卸

时间:2018-12-27 19:54:41

标签: go

概述

如果我有一个带有设置和拆卸逻辑的父级测试,如何在其中并行运行子测试,而又不会遇到带有拆卸逻辑的竞争条件?

func TestFoo(t *testing.T) {
    // setup logic
    t.Run("a", func(t *testing.T) {
        t.Parallel()
        // test code
    })
    // teardown logic
}

示例

举一个人为的例子:假设测试需要创建一个tmp文件,该文件将被所有子测试使用,并在测试结束后将其删除。

例如,父测试也调用t.Parallel(),因为这是我最终想要的。但是我的问题和下面的输出是相同的,即使父级不调用t.Parallel()

顺序子测验

如果我按顺序运行子测试,则它们不会通过任何问题:

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "testing"
)

func setup(t *testing.T) (tmpFile string) {
    f, err := ioutil.TempFile("/tmp", "subtests")
    if err != nil {
        t.Fatalf("could not setup tmp file: %+v", err)
    }
    f.Close()
    return f.Name()
}

var ncase = 2

func TestSeqSubtest(t *testing.T) {
    t.Parallel()

    // setup test variables
    fname := setup(t)

    // cleanup test variables
    defer func() {
        os.Remove(fname)
    }()

    for i := 0; i < ncase; i++ {
        t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
            if _, err := os.Stat(fname); os.IsNotExist(err) {
                t.Fatalf("file was removed before subtest finished")
            }
        })
    }
}  

输出:

$ go test subtests  
ok      subtests        0.001s

平行子测验

但是,如果我并行运行子测试,则父测试的拆卸逻辑最终会被称为 之前 ,子测试有机会运行,因此无法运行才能使子测试正常运行。

不幸的是,这种行为符合"Using Subtests and Sub-benchmarks" go博客所说的话:

  

如果测试的测试函数调用了并行测试,则该测试称为并行测试   在test.T实例上使用并行方法。并行测试永远不会   与顺序测试同时运行,其执行是   直到其调用测试功能(父测试的功能)暂停   已经回来了。

func TestParallelSubtest(t *testing.T) {
    t.Parallel()

    // setup test variables
    fname := setup(t)

    // cleanup test variables
    defer func() {
        os.Remove(fname)
    }()

    for i := 0; i < ncase; i++ {
        t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
            t.Parallel() // the change that breaks things
            if _, err := os.Stat(fname); os.IsNotExist(err) {
                t.Fatalf("file was removed before subtest finished")
            }
        })
    }
}

输出:

$ go test subtests  
--- FAIL: TestParallelSubtest (0.00s)
    --- FAIL: TestParallelSubtest/test_0 (0.00s)
        main_test.go:58: file was removed before subtest finished
    --- FAIL: TestParallelSubtest/test_1 (0.00s)
        main_test.go:58: file was removed before subtest finished
FAIL
FAIL    subtests        0.001s

带有WaitGroup的并行子测试

如上面的引文所述,并行子测试要等到父级完成后才能执行,这意味着尝试使用sync.WaitGroup解决并行子测试会导致死锁:

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

    // setup test variables
    fname := setup(t)

    // cleanup test variables
    defer func() {
        os.Remove(fname)
    }()

    for i := 0; i < ncase; i++ {
        t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
            wg.Add(1)
            defer wg.Done()
            t.Parallel()
            if _, err := os.Stat(fname); os.IsNotExist(err) {
                t.Fatalf("file was removed before subtest finished")
            }
        })
    }
    wg.Wait() // causes deadlock
}

输出:

$ go test subtests  
--- FAIL: TestParallelSubtest (0.00s)
    --- FAIL: TestParallelSubtest/test_0 (0.00s)
        main_test.go:58: file was removed before subtest finished
    --- FAIL: TestParallelSubtest/test_1 (0.00s)
        main_test.go:58: file was removed before subtest finished
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
testing.tRunner.func1(0xc00009a000)
        /path/to/golang1.1.11/src/testing/testing.go:803 +0x1f3
testing.tRunner(0xc00009a000, 0xc00005fe08)
        /path/to/golang1.1.11/src/testing/testing.go:831 +0xc9
testing.runTests(0xc00000a0a0, 0x6211c0, 0x3, 0x3, 0x40b36f)
        /path/to/golang1.1.11/src/testing/testing.go:1117 +0x2aa
testing.(*M).Run(0xc000096000, 0x0)
        /path/to/golang1.1.11/src/testing/testing.go:1034 +0x165
main.main()
        _testmain.go:46 +0x13d

goroutine 7 [semacquire]:
sync.runtime_Semacquire(0xc0000a2008)
        /path/to/golang1.1.11/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc0000a2000)
        /path/to/golang1.1.11/src/sync/waitgroup.go:130 +0x64
subtests.TestWaitGroupParallelSubtest(0xc00009a300)
        /path/to/go_code/src/subtests/main_test.go:91 +0x2b5
testing.tRunner(0xc00009a300, 0x540f38)
        /path/to/golang1.1.11/src/testing/testing.go:827 +0xbf
created by testing.(*T).Run
        /path/to/golang1.1.11/src/testing/testing.go:878 +0x353

goroutine 8 [chan receive]:
testing.runTests.func1.1(0xc00009a000)
        /path/to/golang1.1.11/src/testing/testing.go:1124 +0x3b
created by testing.runTests.func1
        /path/to/golang1.1.11/src/testing/testing.go:1124 +0xac

goroutine 17 [chan receive]:
testing.(*T).Parallel(0xc0000f6000)
        /path/to/golang1.1.11/src/testing/testing.go:732 +0x1fa
subtests.TestWaitGroupParallelSubtest.func2(0xc0000f6000)
        /path/to/go_code/src/subtests/main_test.go:85 +0x86
testing.tRunner(0xc0000f6000, 0xc0000d6000)
        /path/to/golang1.1.11/src/testing/testing.go:827 +0xbf
created by testing.(*T).Run
        /path/to/golang1.1.11/src/testing/testing.go:878 +0x353

goroutine 18 [chan receive]:
testing.(*T).Parallel(0xc0000f6100)
        /path/to/golang1.1.11/src/testing/testing.go:732 +0x1fa
subtests.TestWaitGroupParallelSubtest.func2(0xc0000f6100)
        /path/to/go_code/src/subtests/main_test.go:85 +0x86
testing.tRunner(0xc0000f6100, 0xc0000d6040)
        /path/to/golang1.1.11/src/testing/testing.go:827 +0xbf
created by testing.(*T).Run
        /path/to/golang1.1.11/src/testing/testing.go:878 +0x353
FAIL    subtests        0.003s

摘要

那么我该如何在父测试中使用拆解方法,该方法在并行子测试运行后被称为

2 个答案:

答案 0 :(得分:2)

从 Go 1.14 开始,testing.Ttesting.B 具有允许注册拆卸回调的 Cleanup 方法。

t.Cleanup(func() {
    os.Remove(fname)
})

答案 1 :(得分:1)

Go Blog on subtests中提到了如何执行此操作:

func TestParallelSubtest(t *testing.T) {
    // setup test variables
    fname := setup(t)

    t.Run("group", func(t *testing.T) {
        for i := 0; i < ncase; i++ {
            t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) {
                t.Parallel()
                if _, err := os.Stat(fname); os.IsNotExist(err) {
                    t.Fatalf("file was removed before subtest finished")
                }
            })
        }
    })

    os.Remove(fname)
}

博客文章的相关部分在Control of Parallelism下:

  

每个测试都与一个测试功能相关联。如果测试的测试函数在其testing.T实例上调用Parallel方法,则该测试称为并行测试。并行测试永远不会与顺序测试同时运行,并且其执行将被挂起,直到返回其调用测试功能(即父测试的功能)为止。 [...]

     

一个测试将阻塞,直到其测试功能返回并且其所有子测试都已完成。这意味着由顺序测试运行的并行测试将在运行任何其他连续的顺序测试之前完成。

您的问题的具体解决方案可以在Cleaning up after a group of parallel tests部分中找到。