Parallel Sub-tests in Go: A Cautionary Tale
Tags: go, testing, beginners, learning
Published: 2024-06-06 03:46:29

By default Go executes tests sequentially, one test after another. In version 1.17 they added the ability to execute tests in parallel with a super easy and simple one line method `t.Parallel()`.

```
func MyTest(t *testing.T) {
  t.Parallel()
}
```

Go will execute any tests that call this `Parallel` method, well, in parallel, by pausing each test and then resuming when all the sequential (tests that do NOT call the `t.Parallel()` method) have finished execution. 

I won't dive too deep into that subject here, Jetbrains has a wonderful article already written you can read [here](https://blog.jetbrains.com/go/2022/11/22/comprehensive-guide-to-testing-in-go/#running-parallel-tests) that I used as a reference for this post.

**What we're really here to discuss today is how t.Parallel() interacts with sub-tests in go.**

**But first, What is a sub-test and how do I use it?**

A sub-test is a test function inside of a parent test function. You usually find them in situations where there is a common setup requiring a bunch of different conditions to be tested. Its a more readable and cleaner than throwing them all into a single HUGE function. 

Below is a bare-bones example of a sequential test with a subtest:
```
import (
	"fmt"
	"testing"
)

func TestSequential(t *testing.T) {
	fmt.Println("Running main test...")

	t.Run("SubTest", func(t *testing.T) {
		subTest(t)
	})

	fmt.Println("Main test finished!")

}

func subTest(t *testing.T) {
	fmt.Println("Running Subtest!")
}
```

And here is the output of running that test. It's what you would expect:
```
Running main test...
Running Subtest!
Main test finished!
```

**"Wow, that seems pretty useful!... wait, didn't you say this was a cautionary tale? What could go wrong with that?" You might be asking yourself.**

Just like regular tests, you can also make sub-tests parallel by simply calling the `t.Parallel()` method! Pretty sweet right? 

Well, you'd think so. **But be warned. Parallel sub-tests play by their own rules!**

Parallel sub-tests execute **AFTER** its main parent test has finished executing. And, its only after going slightly insane over a bug in one of our tests, that I stumbled upon this nugget of knowledge buried deep in a [go dev blog](https://go.dev/blog/subtests) in the `Control of Parallelism` section.

The below simple test set up demonstrates this issue:
```
func TestParallelSubTests(t *testing.T) {
	fmt.Println("Starting main test...")
	t.Run("SubTestOne", func(t *testing.T) {
		testOne(t)
	})

	t.Run("SubTestTwo", func(t *testing.T) {
		testTwo(t)
	})

	fmt.Println("Main test done!")
}

func testOne(t *testing.T) {
	t.Parallel()
	fmt.Println("Running testOne!")
}

func testTwo(t *testing.T) {
	t.Parallel()
	fmt.Println("Running testTwo!")
}
```

Based on our previous sub-test example, what do you think the output will be?

Well, as it happens. It actually executes these two sub-tests, `testOne` and `testTwo` **AFTER** the main test, `TestParallelSubTests` has already finished its execution. 

Don't take my word for it. Here are the logs.
```
Starting main test...
Main test done!
Running testOne!
Running testTwo!
```

**Things get even more confusing when you throw a defer statement in the mix**

I've previously gone over `defer` statements in go and how they, like these parallel tests, also execute after the containing method has finished its execution. You can read more about that [here](https://dev.to/blazingbits/defer-statements-in-go-a-short-crash-course-3khl).

So what happens when you mix the two, parallel sub-tests and defer statements?

They both execute after the main test has finished its execution, but it seems that `defer` statements will execute **BEFORE** your sub-tests. 

Here is the test setup, it's the same as the previous setup, but with an added defer statement:
```
func TestParallelSubTests(t *testing.T) {
	fmt.Println("Starting main test...")
	t.Run("SubTestOne", func(t *testing.T) {
		testOne(t)
	})

	t.Run("SubTestTwo", func(t *testing.T) {
		testTwo(t)
	})

	defer deferredPrint()

	fmt.Println("Main test done!")
}

func testOne(t *testing.T) {
	t.Parallel()
	fmt.Println("Running testOne!")
}

func testTwo(t *testing.T) {
	t.Parallel()
	fmt.Println("Running testTwo!")
}

func deferredPrint() {
	fmt.Println("Deferred method!")
}
```

And here is its resulting output:
```
Starting main test...
Main test done!
Deferred method!
Running testOne!
Running testTwo!
```

**So dear reader, BEWARE!**

The parallel sub-test execution order can cause some serious stress induced headaches if you're unaware. Even more so when you're using it in conjunction with `defer` statements. 

Its especially concerning when you consider that the main use case of these sub-tests is the common setup and teardown of testing data or infrastructure. If you're caught off guard you may be tearing down your test environment before running any tests!