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:
| Situation | Parallel? |
|---|---|
| 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 context | Parallel safe? | Recommended fix |
|---|---|---|
| Class fields | ✔ Yes | none |
IClassFixture | ✔ Yes | none |
ICollectionFixture | ⚠ Maybe | Put all into 1 collection |
| Static fields | ❌ No | lock / collection / disable |
| Database | ❌ No | Collection |
| File system | ❌ No | Unique dirs / collection |
| External API | ❌ No | Mock or collection |
| Timing / async-heavy tests | ⚠ Risky | conservative scheduler |