Skip to content

ADR 082: A unit test process inherits nothing

This page is generated from docs/decisions/*.yaml by task docs:export-adr-markdown. Do not edit manually.

  • Number: 082
  • Title: A unit test process inherits nothing
  • Category: development
  • Status: accepted
  • Provenance: guided-ai
  • Source: docs/decisions/082-a-unit-test-process-inherits-nothing.yaml

Decision

A package whose tests run the CLI seals its process in TestMain: the credentials, host and repository context are emptied, the stored config is disabled, and the retry policy is set to none. A test then says what it wants by passing it -- through the Dependencies seam a command is built with, through a flag, or through the config.Overrides a root command is constructed with -- and never by publishing it to the process. The last of those is how a credential travels, because a password is never a flag value (ADR-047) and the environment is shared. t.Setenv is reserved for tests whose subject is the environment, and those tests accept that they run alone, because the call disqualifies a test from t.Parallel and every helper that reaches it taints its callers.

Agent Instructions

Do not open a test with t.Setenv("SOMETHING", "") to clear what the process might be carrying; the seal has already cleared it. Do not set BITBUCKET_URL, BITBUCKET_TOKEN, BITBUCKET_PROJECT_KEY or BITBUCKET_REPO_SLUG to give a command its configuration -- pass them, and add a field to the package's test setup if there is nowhere to put them. A new package under internal/cli/cmd gets the sealed TestMain when it is created. When a value can only reach the code through the environment, that is a missing seam in the code and not a reason to publish it.

Rationale

Three things were true of this suite and none of them were intended. The environment decided what a test saw: config.LoadWithOverrides loads .env itself, walking up from the working directory, so go test ./... in a checkout with a configured live stack ran the same tests against different inputs than the same command on a clean machine. Tests defended against that one at a time, six t.Setenv(key, "") calls at the top of each, which is the call that stops a test declaring itself parallel -- so the defence against the first problem caused the second. And a value that could only arrive through the environment marked a place where the code had no way to be told, which is the same gap that had left production reading BITBUCKET_REPO_SLUG past the layer that resolves everything else (ADR-065). Sealing the process answers all three: nothing is inherited, nothing has to be cleared, and a test that cannot pass what it needs has found a seam worth adding.

Rejected Alternatives

  • Keep t.Setenv and accept a sequential suite: What was in force, by default rather than by decision. It cost the unit suite roughly ten seconds a run and the live suite eight minutes, on every commit and every pull request. It also left the inheritance problem in place, which is the part that makes a green run on one machine no evidence about another.
  • Set the environment once in TestMain and leave the values there: Kept, but only for values that never vary: the seal writes empties and a retry count, and the live suite writes its URL and credentials, because nothing changes them afterwards so no test can observe another's. It does not extend to a repository context, which differs per test and is exactly what two parallel tests would take from each other.
  • Run each package's tests in their own process: Go already does that -- the isolation problem is between tests inside one binary, which a second binary does not solve. It would also multiply the compile and link time that is already most of what a warm unit run costs.