Why does t.Setenv panic after t.Parallel?
Go’s testing package tracks whether a test is parallel or mutates global state, and panics if you do both in the same test.
Last week, I debugged a flaky test caused by state leaking from another test. One test called os.Setenv with an environment variable that affected the behaviour of the other test. Although the first test cleaned up after itself, the two tests could run in parallel, and if they ran at the same time the second test would fail.
In general, Go tests should use t.Setenv instead of os.Setenv. t.Setenv automatically restores the original value at the end of a test, and prevents parallel execution. Switching to t.Setenv was enough to fix this specific flake, but while looking for similar mistakes, I learnt more about how Go handles parallel tests.
In some of our tests, the os.Setenv call happens deep inside non-test code and it’s awkward to change, so we use a different helper to prevent leaking state:
23// AssertNotParallel asserts that t has not been marked as parallel.
24// It panics (via t.Setenv) if t.Parallel has already been called.
25//
26// Use this when a test modifies package-level globals or other shared
27// state that would be unsafe to modify concurrently with other tests.
28func AssertNotParallel(t testenv.TB) {
29 t.Helper()
30 t.Setenv("ASSERT_NOT_PARALLEL_TEST", "1") // panics if t.Parallel was called
31}tstest/tstest.go in the tailscale/tailscale repo. Copyright Tailscale Inc & contributors, used under the BSD-3-Clause license. Here testenv.TB is a copy of testing.TB, but without a testing dependency so it can be used in non-test code.I was intrigued by the comment: why does t.Setenv panic if t.Parallel has already been called?
Reading the source code for the testing package makes this clear. The testing.T struct tracks parallel status with two private fields:
isParallel– set when you callt.Parallel().denyParallel– set when calling operations that mutate global state, specificallyt.Chdirandt.Setenv.
These fields are mutually exclusive.
The functions t.Parallel, t.Setenv and t.Chdir inspect both flags, and panic if you try to use both in a single test:
package main
import (
"testing"
)
func TestCannotSetenvAfterParallel(t *testing.T) {
t.Parallel()
t.Setenv("COLOUR", "red")
// panics: "test using t.Setenv or t.Chdir can not use t.Parallel"
}
func TestCannotParallelAfterSetenv(t *testing.T) {
t.Setenv("COLOUR", "red")
t.Parallel()
// panics: "test using t.Setenv or t.Chdir can not use t.Parallel"
}Now I understand how AssertNotParallel works. It triggers t.Setenv, which inspects isParallel and panics if the test is marked parallel. The protection is also stronger than the doc comment implies – calling AssertNotParallel and t.Setenv sets denyParallel, which blocks any subsequent calls to t.Parallel().
There’s currently an open proposal to add an explicit t.Serial method to the testing package, and I hope it’s accepted. Mutating dummy environment variables is a hack that works, but a dedicated t.Serial method is a much clearer statement of intent.