Go - Running Tests in Parallel

发布时间 2023-10-18 17:01:40作者: ZhangZhihuiAAA

Problem: You want to speed up testing by running tests in parallel.


Solution: Use the t.Parallel function to enable tests or subtests to run in parallel.

 

By default, test functions in the same package are run sequentially. Go 1.7 included a new function, t.Parallel , that allows test functions to be run in parallel. Doing this is very straightforward. You only need to add a line that calls t.Parallel in your test functions. Here is a quick look at some simple test functions:

func TestAddOneDigit(t *testing.T) {
    result := Add(1, 2)
    if result != 3 {
        t.Error("Adding  1  and  2  doesn't  produce  3")
    }
}

func TestAddTwoDigits(t *testing.T) {
    result := Add(12, 30)
    if result != 42 {
        t.Error("Adding  12  and  30  doesn't  produce  42")
    }
}

func TestAddThreeDigits(t *testing.T) {
    result := Add(100, -1)
    if result != 99 {
        t.Error("Adding  100  and  - 1  doesn't  produce  99")
    }
}

You also add a 0.5 - second delay in the Add function to highlight the test timing:

func Add(a, b int) int {
    time.Sleep(500 * time.Millisecond)
    return a + b
}

If you run the tests, you can see that the tests are run in sequence. Including other overheads, the overall test timing is almost 2 seconds:
% go test - v

=== RUN TestAddOneDigit

- - - PASS: TestAddOneDigit (0.50s)

=== RUN TestAddTwoDigits

- - - PASS: TestAddTwoDigits (0.50s)

=== RUN TestAddThreeDigits

- - - PASS: TestAddThreeDigits (0.50s)

PASS

ok github.com/sausheong/gocookbook/ch18_testing 1.997s

By adding a single line in each of the test functions, you can run the code in parallel:

func TestAddOneDigit(t *testing.T) {
    t.Parallel()
    result := Add(1, 2)
    if result != 3 {
        t.Error("Adding  1  and  2  doesn't  produce  3")
    }
}

func TestAddTwoDigits(t *testing.T) {
    t.Parallel()
    result := Add(12, 30)
    if result != 42 {
        t.Error("Adding  12  and  30  doesn't  produce  42")
    }
}

func TestAddThreeDigits(t *testing.T) {
    t.Parallel()
    result := Add(100, -1)
    if result != 99 {
        t.Error("Adding  100  and  - 1  doesn't  produce  99")
    }
}

Now run it again:
% go test - v

=== RUN TestAddOneDigit

=== PAUSE TestAddOneDigit

=== RUN TestAddTwoDigits

=== PAUSE TestAddTwoDigits

=== RUN TestAddThreeDigits

=== PAUSE TestAddThreeDigits

=== CONT TestAddOneDigit

=== CONT TestAddTwoDigits

=== CONT TestAddThreeDigits

- - - PASS: TestAddTwoDigits (0.50s)

- - - PASS: TestAddOneDigit (0.50s)

- - - PASS: TestAddThreeDigits (0.50s)

PASS

ok github.com/sausheong/gocookbook/ch18_testing 0.686s

You can see how the test functions now run in parallel, and the timing is reduced to almost 0.5 seconds.

You can also run subtests in parallel, but you need to be careful because there’s a big gotcha here. Let’s take whatever you’ve done in the test functions and translate that into subtests, making it run in parallel:

func TestAddWithSubTestAndParallel(t *testing.T) {
    testCases := []struct {
        name string
        a int
        b int
        result int
    }{
        {"OneDigit", 1, 2, 3},
        {"TwoDigits", 12, 30, 42},
        {"ThreeDigits", 100, -1, 99},
    }

    for _, testCase := range testCases {
        t.Run(testCase.name, func(t *testing.T) {
            t.Parallel()
            result := Add(testCase.a, testCase.b)
            if result != testCase.result {
                t.Errorf("Adding  %d  and  %d  doesn't  produce  %d, instead  it  produces  %d",
                    testCase.a, testCase.b, testCase.result, result)
            }
        })
    }
}

When you run it, it looks fine:
% go test - v - run TestAddWithSubTestAndParallel

=== RUN TestAddWithSubTestAndParallel

=== RUN TestAddWithSubTestAndParallel/OneDigit

=== PAUSE TestAddWithSubTestAndParallel/OneDigit

=== RUN TestAddWithSubTestAndParallel/TwoDigits

=== PAUSE TestAddWithSubTestAndParallel/TwoDigits

=== RUN TestAddWithSubTestAndParallel/ThreeDigits

=== PAUSE TestAddWithSubTestAndParallel/ThreeDigits

=== CONT TestAddWithSubTestAndParallel/OneDigit

=== CONT TestAddWithSubTestAndParallel/TwoDigits

=== CONT TestAddWithSubTestAndParallel/ThreeDigits

- - - PASS: TestAddWithSubTestAndParallel (0.00s)

- - - PASS: TestAddWithSubTestAndParallel/TwoDigits (0.50s)

- - - PASS: TestAddWithSubTestAndParallel/OneDigit (0.50s)

- - - PASS: TestAddWithSubTestAndParallel/ThreeDigits (0.50s)

PASS

ok github.com/sausheong/gocookbook/ch18_testing 0.718s

This looks fine because you know if it doesn’t run in parallel, it would take almost 2 seconds. But is it really OK? Let’s check by adding a single line to check the actual test case that was run in each subtest:

... 
t . Parallel () 
t . Logf ( "Test  case  %s  with  inputs  %d  and  %d  should  produce  %d" , 
      testCase . name ,   testCase . a ,   testCase . b ,   testCase . result ) 
result   :=   Add ( testCase . a ,   testCase . b ) 
...

Run it again and see the results:
% go test - v - run TestAddWithSubTestAndParallel

=== RUN TestAddWithSubTestAndParallel

=== RUN TestAddWithSubTestAndParallel/OneDigit

=== PAUSE TestAddWithSubTestAndParallel/OneDigit

=== RUN TestAddWithSubTestAndParallel/TwoDigits

=== PAUSE TestAddWithSubTestAndParallel/TwoDigits

=== RUN TestAddWithSubTestAndParallel/ThreeDigits

=== PAUSE TestAddWithSubTestAndParallel/ThreeDigits

=== CONT TestAddWithSubTestAndParallel/OneDigit

=== CONT TestAddWithSubTestAndParallel/ThreeDigits

=== CONT TestAddWithSubTestAndParallel/TwoDigits

=== CONT TestAddWithSubTestAndParallel/OneDigit

testing_test.go:108: Test case ThreeDigits with inputs 100 and - 1 should produce 99

=== CONT TestAddWithSubTestAndParallel/TwoDigits

testing_test.go:108: Test case ThreeDigits with inputs 100 and - 1 should produce 99

=== CONT TestAddWithSubTestAndParallel/ThreeDigits

testing_test.go:108: Test case ThreeDigits with inputs 100 and - 1 should produce 99

- - - PASS: TestAddWithSubTestAndParallel (0.00s)

- - - PASS: TestAddWithSubTestAndParallel/ThreeDigits (0.50s)

- - - PASS: TestAddWithSubTestAndParallel/TwoDigits (0.50s)

- - - PASS: TestAddWithSubTestAndParallel/OneDigit (0.50s)

PASS

ok github.com/sausheong/gocookbook/ch18_testing 0.691s

The last test case was run three times in parallel! What happened? This happened because you are trying to use a goroutine (by calling t.Run ) on a loop iterator variable (the testCase variable). The second parameter in t.Run is a closure that is bound to the same testCase variable in every iteration, and a pointer to this variable is passed into the closure. The iterator and the goroutines run independently, and the iterator (in this case) ran and finished faster than the goroutine could even start, so the testCase variable ends up being assigned the last test case. 

This is a well - known problem. Normally, what you should do is pass the variable into the closure by value instead of using it directly from the iterator. However, this is not possible here because t.Run expects a function with only one parameter, which is testing.T . So how can you avoid this?

The simplest fix is to make testCase a local variable within the loop instead. This is because variables declared within the loop are not shared between iterations:

    for _, tc := range testCases {
        testCase := tc
        t.Run(testCase.name, func(t *testing.T) {
            t.Parallel()
            t.Logf("Test  case  %s  with  inputs  %d  and  %d  should  produce  %d",
                testCase.name, testCase.a, testCase.b, testCase.result)
            result := Add(testCase.a, testCase.b)
            if result != testCase.result {
                t.Errorf("Adding  %d  and  %d  doesn't  produce  %d,  instead  it  produces  %d", testCase.a, testCase.b, testCase.result, result)
            }
        })
    }