Why does a Go test that calls t.Setenv panic if it also calls t.Parallel?
answer
- a process has one environment
- restoring assumes nobody else looked
- the panic fires in either order
- the working-directory helper has the same rule
basics
~20 sEnvironment variables belong to the whole process, so one test's t.Setenv value would leak into every test running at the same time. The testing package therefore panics when a test combines t.Setenv with t.Parallel, in either order.
solid answer
~50 s`t.Setenv` sets a real process environment variable and registers a cleanup that restores the previous value when the test ends. That restore-afterwards trick is only sound if nobody else observes the variable in between, and under `t.Parallel()` other tests are running in the very same process, so they would see the wrong value at an unpredictable moment. Rather than let that corrupt a neighbour silently, `testing` panics: calling `t.Setenv` in a test that has already called `t.Parallel` panics, and calling `t.Parallel` after `t.Setenv` panics too. `t.Chdir`, which changes the process working directory, is governed by the same rule. The way out is not to force it: keep that test sequential, or change the code under test to accept the directory or configuration as an argument so the test never touches process-wide state and can be parallel.
code
go · 5 linesfunc TestLoadShard(t *testing.T) {
t.Setenv("ETL_INPUT_DIR", "testdata/shard-a")
t.Parallel() // panics: t.Parallel after t.Setenv is not allowed
// ...
}go deeper
Remember the rule itself: a test cannot both set an environment variable through t.Setenv and call t.Parallel, and trying it panics. Knowing that the environment is process-wide is the reason to recall.
Explain the mechanism: t.Setenv mutates real process state and restores it afterwards, which only works if no other test observes it meanwhile. Be ready to say that both orderings panic and that t.Chdir follows the same rule.
Show how you would remove the need for process state at all — configuration read once at the edge and passed as arguments — and be able to name the unguarded cases, such as os.Setenv, package-level variables and fixed ports, where nothing panics for you.
Own the convention: whether the codebase reads environment variables deep inside packages or only at the entry point decides how much of the suite can ever be parallel. Argue that cost against the churn of refactoring configuration plumbing.
## The two mechanisms in conflict `t.Setenv(key, value)` is a convenience over `os.Setenv`: it records the current value of the variable, sets the new one, and arranges for the old value (or its absence) to be restored when the test finishes. `t.Chdir(dir)` does the same trick for the process working directory. Both are shorthands for *mutate global state, then put it back*. `t.Parallel()` says the opposite thing: *other tests may be running inside this same process at the same time as me.* Those two cannot both be true safely. A process has exactly one environment block and exactly one working directory. There is no per-test, per-goroutine copy. If a parallel test flips `ETL_INPUT_DIR` for a few milliseconds, every other test running concurrently that reads it sees the flipped value, and the restore at the end of the test does nothing to help them — the damage happened while the value was set. Worse, the failure is timing-dependent: it shows up on a loaded CI machine and not on a laptop. ## What the testing package does about it Instead of documenting the hazard and hoping, `testing` makes the combination impossible to write. It panics, and it does so in **both** orders: - `t.Parallel()` first, then `t.Setenv(...)` in the same test: panic. - `t.Setenv(...)` first, then `t.Parallel()`: also panic, because the test has already been marked as having changed the environment. `t.Chdir` behaves identically with respect to the working directory. The panic is deliberate and immediate: a panic during a test fails that test loudly with a stack trace pointing at the offending call, which is far better than a neighbouring test failing for reasons that have nothing to do with its own code. Note that the ban is scoped to the test that calls it, including through subtests: a test cannot be parallel and set the environment, and a parent cannot set the environment and then run parallel children that would outlive its body. ## What it does not stop The guard only covers the two helpers `testing` knows about. A parallel test that calls `os.Setenv` or `os.Chdir` directly gets no panic and no protection at all — it simply corrupts its neighbours. So the rule is not "`testing` protects me from global state"; it is "`testing` refuses the two cases it can see, and everything else is on you." Package-level variables, registered singletons, a shared temp path, a fixed TCP port and the seed of a shared random source are all the same class of problem with no guard rail. ## Getting the parallelism anyway There are three honest routes, in increasing order of value: 1. **Leave the test sequential.** If exactly two tests in the package need an environment variable, do not put `t.Parallel()` in them. The suite loses almost nothing. 2. **Read the environment once, at the edge.** Have `main` or a small `Config` loader read the variables and pass a plain struct or a string argument down. Then the tests exercise a function with an argument, need no process state, and can all be parallel. This is the fix that usually pays for itself, because it also makes the production code easier to configure. 3. **Isolate at the process boundary.** Different Go packages are compiled into different test binaries and run as separate processes, so a package whose tests genuinely must own the environment is already isolated from other packages. Pushing such tests into their own package is a legitimate structural answer. The same reasoning applies to the working directory: instead of `t.Chdir` plus relative paths, pass the directory into the function you are testing and build paths from it, and the test becomes both parallel-safe and clearer about what it depends on. ## Interview framing The question is really about recognising which state is per-test and which is per-process. Slices, maps and structs created inside a test body are per-test unless something else can reach them; the environment, the working directory, package-level variables, and anything a library keeps in a global registry are per-process, and no amount of cleanup makes them safe to mutate while a sibling is running.
- Does the same restriction apply if the test calls os.Setenv directly instead of t.Setenv?There is no panic, and no protection either. `os.Setenv` in a parallel test silently mutates the process environment for every concurrently running test, and nothing restores it when the test ends. The guard exists only in the `testing` helpers, so calling the `os` functions is strictly worse.
- How would you restructure a test that needs an environment variable so it can still be parallel?Stop reading the environment inside the code under test. Have the caller read the variables once into a config struct and pass the values as arguments; the test then calls a pure-ish function with explicit inputs, needs no process state, and is parallel-safe. Failing that, keep the test sequential or move it to its own package.
- Why is t.Chdir subject to the same rule?The working directory is a single process-wide attribute, exactly like the environment. Changing it under `t.Parallel()` would make every concurrently running test resolve relative paths against a directory it did not choose, so `t.Chdir` panics in a parallel test for the same reason `t.Setenv` does.
saying these in an interview costs you the question
- Thinks each test gets its own copy of the environment
- Believes the cleanup restore makes it safe under concurrency
- Says only one of the two orderings panics
- Reaches for os.Setenv to dodge the panic
- Assumes goroutines can have separate working directories