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