NashTech Blog

Table of Contents

Understanding Parallel Test Execution in xUnit: Concepts, Pitfalls & Best Practices

Modern applications grow fast, and so do their test suites. Running hundreds or thousands of tests sequentially can be slow and inefficient. Fortunately, xUnit.net offers built-in test parallelization to help reduce your test execution time dramatically by using available CPU threads more effectively.

But with great speed comes great responsibility: parallel tests can introduce race conditions, flakiness, and other hard-to-debug problems — especially when tests share memory or shared context.

This blog explains:

  • How xUnit runs tests in parallel
  • All types of shared context (fixtures, static data, external resources)
  • When parallel execution is safe
  • How to handle shared memory correctly
  • Examples for every well-known parallel case

Let’s dive in.


1. How xUnit Handles Parallel Execution

Parallel test execution in xUnit is based on the concept of test collections.

Default rules:

SituationParallel?
Tests within a test class❌ No (always sequential)
Tests between different classes✔️ Yes (can run concurrently)
Tests in same test collection❌ No
Tests in different collections✔️ Yes

Why?

Each test class is treated as a test collection by default. Collections cannot run tests in parallel with themselves, but they can run tests in parallel with other collections.

Example

Two classes → test classes run in parallel:

public class ClassA {
    [Fact] public void TestA1() { }
}

public class ClassB {
    [Fact] public void TestB1() { }
}

Tests inside ClassA are sequential.

Tests inside ClassB are sequential.

But ClassA and ClassB run in parallel.


2. Configuring Parallel Behavior

You can tailor parallel execution at the assembly level:

Disable all parallelism

[assembly: CollectionBehavior(DisableTestParallelization = true)]

Put all classes in one collection (sequential execution)

[assembly: CollectionBehavior(CollectionPerAssembly = true)]

Limit number of parallel threads

[assembly: CollectionBehavior(MaxParallelThreads = 4)]

Choose scheduling algorithm (xUnit 2.8+)

  • Conservative (default): safer for async + timeouts
  • Aggressive: starts more tests aggressively for speed

3. Understanding Shared Contexts in xUnit

Parallel tests are harmless unless they share context.

“Shared context” means any state or resource accessed by multiple tests:

Shared in-memory state

  • class fields
  • static fields
  • shared singletons

xUnit fixtures

  • IClassFixture<T>
  • ICollectionFixture<T>
  • Assembly-level fixtures (in xUnit v3)

External shared resources

  • databases
  • files / folders
  • caches
  • external API
  • test server

Different types of shared context require different handling.


4. Types of Shared Context & How to Handle Them


A. Shared context inside a test class

Tests in the same class never run in parallel → safe.

public class MyTests {
    private int counter = 0;

    [Fact] public void A() { counter++; }
    [Fact] public void B() { counter++; }
}

✔ No locks needed

✔ Safe by design

When to use

  • You want sequential execution
  • Shared expensive setup inside a single class

B. Shared context with IClassFixture<T>

Fixture instance is shared across tests in a class, but tests are still sequential.

public class DbFixture { public DbConnection Conn; }
public class DbTests : IClassFixture<DbFixture> { }

✔ Safe

✔ No locking required

When to use

  • Shared DB setup for tests in a class
  • Heavy initialization you want to run once per class

C. Shared context with ICollectionFixture<T>

A collection fixture is shared across multiple test classes.

These classes run sequentially only if they share the same collection name.

[Collection("Db")]
public class UserTests : ICollectionFixture<DbFixture> { }

[Collection("Db")]
public class OrderTests : ICollectionFixture<DbFixture> { }

✔ These two classes run sequentially

✔ Other classes run in parallel

When to use

  • Shared DB
  • Shared external resources
  • Shared static/global state

Danger

If another class uses a different collection, it may still run in parallel and interfere.


D. Shared static data (most dangerous)

public static int Count = 0;

🚨 Parallel tests from different classes may corrupt state.

Fix options:

1. Lock shared data

private static readonly object _lock = new();
public static void Increment()
{
    lock (_lock) { Count++; }
}

2. Put all tests accessing the static resource in the same collection

[Collection("StaticTests")]
public class StaticSharedTests { }

3. Disable parallel execution

[assembly: CollectionBehavior(DisableTestParallelization = true)]

E. Shared External Resources (DB, Files, API)

Examples:

  • writing to same folder
  • writing/reading same DB
  • modifying global config
  • shared HTTP test server

These are never safe to use in parallel.

Best fix:

Isolate them in a single collection.

[Collection("DatabaseTests")]
public class DbTests1 { }

[Collection("DatabaseTests")]
public class DbTests2 { }

All DB tests → sequential

All other tests → parallel

➡ Best performance

➡ No race conditions


5. Full Combined Example: Every Parallel Case

Below is a complete sample showing all major parallel scenarios.


Case 1: Default parallel execution

public class TestClassA {
    [Fact] public void A1() { Thread.Sleep(3000); }
    [Fact] public void A2() { Thread.Sleep(2000); }
}

public class TestClassB {
    [Fact] public void B1() { Thread.Sleep(3000); }
    [Fact] public void B2() { Thread.Sleep(2000); }
}

  • A1/A2 sequential
  • B1/B2 sequential
  • ClassA & ClassB run in parallel

Case 2: Disable all parallelism

[assembly: CollectionBehavior(DisableTestParallelization = true)]


Case 3: Group tests into a shared collection

[Collection("DbTests")]
public class DbTest1 { }

[Collection("DbTests")]
public class DbTest2 { }

✔ Sequential

✔ Safe for shared DB

✔ Other tests stay parallel


Case 4: Thread-safe shared resource

public class CacheFixture
{
    private readonly object _lock = new();

    public void Add(string key, object value)
    {
        lock (_lock) {
            // safe writes
        }
    }
}

public class CacheTests : IClassFixture<CacheFixture> { }


Case 5: Best practice mixed strategy

Allow parallelism globally:

[assembly: CollectionBehavior(MaxParallelThreads = 4)]

Protect shared areas:

[Collection("SharedDb")]
public class DbTests1 { }

[Collection("SharedDb")]
public class DbTests2 { }

This gives you:

✔ Maximum speed

✔ Safe handling of shared memory

✔ Clean structure for large test suites


6. Summary: What to Use & When

Shared contextParallel safe?Recommended fix
Class fields✔ Yesnone
IClassFixture✔ Yesnone
ICollectionFixture⚠ MaybePut all into 1 collection
Static fields❌ Nolock / collection / disable
Database❌ NoCollection
File system❌ NoUnique dirs / collection
External API❌ NoMock or collection
Timing / async-heavy tests⚠ Riskyconservative scheduler

Picture of Phuong Hoang Tang Kim Nam

Phuong Hoang Tang Kim Nam

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top