Skip to content

proposal: testing: run tests in parallel by default #73805

Open
@CAFxX

Description

@CAFxX

Important

To clear any misunderstanding: this proposal aims at not changing the semantics of existing tests.

Proposal Details

#21214 proposed the same but was declined -- mostly because at the time we had no good way of rolling out such a change safely. As this is not the case anymore, and the benefits keep increasing with time (core counts are increasing, codebases and test suites are growing, etc.), I think it would be worth reconsidering it.

The idea would be to make parallel execution the default for modules that specify a go version (not toolchain version) higher than v1.X. Modules with go version below v1.X would still default to serial execution, as today. This would mean that behaviour would not change until a module go version (not toolchain version) is manually updated to v1.X or later.

Individual tests would be given a way to opt-out of parallel execution, similar to how today we allow individual tests to opt-in to parallel execution.

A one-off migration executed automatically when the module is upgraded to a go version after v1.X would ensure that existing semantics (serial execution) are maintained.

I encourage reviewers to focus on the goal when discussing this proposal, i.e. I welcome alternative ways/solutions to achieve the same goal, and will gladly update the proposal if a better solution is suggested. Some discussion is happening in https://gophers.slack.com/archives/C0VP8EF3R/p1747791714490489.

Proposal

  • Add testing.T.Serial() to allow individual tests to opt out of the new default. Mention that Serial was the default before go v1.X. No-op if the default is serial execution. Panics or fails the test if called after Parallel(). If present, it must be the first statement in the test function.
  • Update the doc for testing.T.Parallel() to mention that this is the default starting with go v1.X. No-op if the default is parallel execution. Panics or fails the test if called after Serial().
  • For modules that specify in go.mod a go version equal or greater than v1.X, run the tests in parallel by default; modules that specify an older version would still run tests serially be default.
  • When a module's go version is updated with go mod tidy (when it triggers the bump) or go mod edit -go=1.Z1 from a v1.Y < v1.X to a version v1.Z >= v1.X, a one-off migration is performed2 on the module, adding Serial() calls to any test that does not have a Parallel() call, and removing Parallel() calls from any test that has it3.
  • Linters and other tools could start flagging t.Parallel() calls as unneeded for modules with a go.mod go version equal or greater than v1.X.

To guarantee the ordering of serial tests, the compiler should recognize test functions that call t.Serial() and arrange for them to be executed in the order in which they appear in the file. (This is currently the ugliest implementation detail of this proposal; alternatives are most welcome).

FAQ

Will I be forced to rewrite my tests to be runnable in parallel?

No.

A migration tool would automatically turn existing tests like:

func TestA(t *testing.T) {
  // ...
}

func TestB(t *testing.T) {
  t.Parallel()
  // ...
}

into:

func TestA(t *testing.T) {
  t.Serial()
  // ...
}

func TestB(t *testing.T) {
  // ...
}

thus maintaining the current semantics, even after the migration to the new go version.

For cases in which t.Parallel() is not the first statement with side effects in the function there are two options: either bail out of the migration and warn the user that manual migration is required (but this is pretty disruptive), or drop the t.Parallel() and as a fallback add a t.Serial(), printing a warning about this (as it may slow the test suite).

Users could then, over time, drop the t.Serial from tests that do not really need them.

Worth pointing out that, even without version control, this change is trivially reversible3.

Will this break my tests?

No. (see previous answer for details)

Why should tests be executed in parallel by default?

Because serial tests:

  • can accidentally hide hidden dependencies between tests
  • can accidentally hide data races
  • implicitly endorse the use of global state
  • are slower to run when part of a large test suite4

Parallel tests being the default nudge the whole ecosystem in a better direction, without forcing anyone to have to write parallel tests.

Isn't this going to be disruptive?

Statistics indicate that the vast majority of modules do not specify the most recent go version (1.24.*), or even just a go module version that was released in the last year.

go.mod go version module count5
1.24 58.1k
1.23 179k
1.22 177k
1.21 150k
1.20 101k
1.19 88.1k
1.18 90.6k
1.17 82.9k
1.16 92.2k
1.15 90.1k
1.14 68.9k
1.13 77.3k
1.12 42.0k
1.11 4.4k
1.10 0.2k
1.9 0.2k
1.8 <0.1k

This would seem to suggest that most existing modules will not be migrated to a go module version where the default is parallel execution. For modules that are migrated, the automated migration would make sure that existing semantics are maintained, therefore not requiring maintainers to spend any extra effort during the migration or later - unless they want to make use of parallel test execution (but this is orthogonal to the change, as it would be the case even if this proposal is not enacted).

I can acknowledge that some users that do not follow go development may be surprised by the new t.Serial() calls added by the migration tool, but I would suggest that a quick online search would almost certainly lead them to an article that explains the rationale.

All the above considered, I would argue that the change may be at most characterized as "surprising", but not really "disruptive".

Footnotes

  1. We could detect also manual edits of go.mod if the module's go version was added to go.sum (or a different "lock" file). In this way the go tool could notice manual modifications of the go version, and either direct the user to run the migration using a dedicated command, or prompt the user that to continue the action a migration is required that will be executed if the user accepts.

  2. Whether the migration is performed silently, or whether the user is prompted to confirm they want to run the migration, is a UX implementation detail.

  3. This migration is trivially reversible even without version control by adding a Parallel() call to any function without Serial() call, and removing all Serial() calls. 2

  4. This is further made relevant by the increasing core counts of modern infrastructure. Making use of the available cores to reduce test duration is much easier if most/all tests are executed in parallel. While it is true that go test is normally already able to parallelize test execution across packages, this does not really help much in a number of scenarios, e.g. when there are few packages to test (common in edit/test cycles), or when a package takes significantly longer to be tested than all others.

  5. Module count obtained on 2025-05-29 by searching on public Github repos using the search query language:"Go Module" /^go 1\.x/ where x is replaced by the go minor version (e.g. 24 for 1.24)

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolPerformanceProposal

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions