Skip to content

Build1 publisher3 min readPublished

xUnit 4's ParallelMode.All is an isolation change wearing a performance flag's clothes

Full test-case parallelization stays off by default in xUnit.net v3 4.0.0. The assembly attribute that turns it on also withdraws the guarantee that two rows of one theory will not overlap.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • The xUnit.net v3 4.0.0 release notes describe full test-case parallelization as a new feature.
  • The default parallelization mode is still ParallelMode.Collections, so upgrading does not silently enable the broader mode.
  • With ParallelMode.Collections, tests within a collection are serialized. With ParallelMode.All, every test case is eligible to run beside every other test case, including two cases from the same class and two pre-enumerated rows from the same theory.
  • Opting in is done at the assembly level, using Xunit.Sdk and Xunit.v3: [assembly: Parallelization(Mode = ParallelMode.All, MaxThreads = 2, Algorithm = ParallelAlgorithm.Conservative)].
  • The author treats the switch as an isolation change, not a speed switch, and wants a deterministic failure that proves the risk plus a deterministic check for each guardrail before enabling it across a suite.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

xUnit.net v3 4.0.0 lists full test-case parallelization as a new feature, and the default remains ParallelMode.Collections, so an upgrade does not silently change how your suite schedules itself [1][2]. The consequence sits in the opt-in: switching to ParallelMode.All does not just add throughput, it withdraws the assumption that two tests in the same class, or two pre-enumerated rows of the same theory, will never overlap [3].

Mechanically the change is three lines. You declare it at the assembly level, for example `[assembly: Parallelization(Mode = ParallelMode.All, MaxThreads = 2, Algorithm = ParallelAlgorithm.Conservative)]`, importing Xunit.Sdk and Xunit.v3 [4]. Under Collections, tests within a collection are serialized; under All, every test case is eligible to run beside every other one [3]. The official parallel test execution guide documents the modes, the algorithms, and the opt-out scopes available [20].

The dev.to write-up by ssukhpinder frames this as an isolation change rather than a speed switch, and that framing does real work [5]. A static fake, a shared fixture, a temporary file with a fixed name, or a database record addressed by a shared ID was safe when the collection serialized its members; it becomes a race the moment the mode flips [6]. The pre-flip inventory he recommends is concrete: mutable static fields, IClassFixture and ICollectionFixture implementations, fixed file names, environment-variable mutation, test servers bound to fixed ports, records keyed by shared IDs, and theory data sources holding objects that rows can mutate [7]. Each item then resolves one of three ways: make the resource concurrency-safe, give it a unique per-test identity, or keep it behind an explicit opt-out. Doing that before a broad CI failure mixes several races into one red build is the cheaper order of operations [8].

Proving the risk needs determinism, not luck. A timing-only test built on Task.Delay can pass for the wrong reason, so the sample uses a Barrier with two participants to force the interleaving: two theory rows read one static counter before either writes, both write afterward, and both then assert the counter equals 2 [10][11]. Both rows observe 0 and both write 1, so the final value is 1 and one increment is lost [11][19]. The barriers make that lost update repeatable on an idle machine, and a ten-second timeout stops a misconfigured run from hanging [12]. The test is deliberately red and lives in a separate project, so the failure is evidence from a verifier rather than a permanent red mark in the guarded suite [13].

The fixes split along the same line. Interlocked.Increment makes a counter atomic, while a lock or a concurrent collection suits a compound invariant, but thread safety is not isolation: two tests can mutate a structure correctly and still see each other's logical data [14][15]. For an exclusive resource, `[Theory(DisableParallelization = true)]` is the targeted opt-out [16]. The sample does not take that attribute on trust. A control configuration compiles the same method without it and uses two barriers to make both rows attempt a one-at-a-time lease before either releases; exactly one row fails, and the normal Release build restores the opt-out so both pass [17].

MaxThreads = 2 in the sample is an inspection aid, not a CI setting; the right number depends on CPU, memory, and the external systems the tests touch [9]. The sample also pins xunit.v3.mtp-v2 at 4.0.0 and selects Microsoft Testing Platform in global.json [18].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories