If you have read my introduction to xUnit.NET, you know that tests run sequentially within a collection. By default, a collection is created for each class, so all tests in a class run one after another. Running all tests in parallel can significantly reduce execution time.
One workaround is to create one class per test, but that is impractical. xUnit 3 plans to support running all tests in parallel (GitHub issue), but that version is still in alpha.
This post is based on the code suggested by Travis Mortimer on GitHub. I refined it for readability and fixed a bug with non-serializable [Theory].
To parallelize all test cases, add the following NuGet package to your project:
csproj (MSBuild project file)
<Project>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Meziantou.Xunit.ParallelTestFramework" Version="1.0.0" />
</ItemGroup>
</Project>
#How does it work?
xUnit is very customizable. You can change the default behavior of the framework by implementing a few interfaces. To change the way tests are executed, override TestFramework.RunTestCases to modify the list of test cases before the framework runs them.
In our case, the strategy is to reassign all test cases to a new collection and let xUnit execute the tests. Since all tests are in different collections, xUnit executes them in parallel. It is that simple!
C#
// Inspired from https://github.com/xunit/xunit/issues/1986#issuecomment-831322722 by Travis Mortimer
namespace Xunit.Custom;
internal sealed class ParallelTestFramework : XunitTestFramework
{
public ParallelTestFramework(IMessageSink diagnosticMessageSink)
: base(diagnosticMessageSink)
{
}
protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
{
return new CustomTestExecutor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
}
private sealed class CustomTestExecutor : XunitTestFrameworkExecutor
{
public CustomTestExecutor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink)
: base(assemblyName, sourceInformationProvider, diagnosticMessageSink)
{
}
protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
{
var newTestCases = SetUpTestCaseParallelization(testCases);
using var assemblyRunner = new XunitTestAssemblyRunner(TestAssembly, newTestCases, DiagnosticMessageSink, executionMessageSink, executionOptions);
await assemblyRunner.RunAsync();
}
/// <summary>
/// By default, all test cases in a test class share the same collection instance which ensures they run synchronously.
/// By providing a unique test collection instance to every test case in a test class you can make them all run in parallel.
/// </summary>
private IEnumerable<IXunitTestCase> SetUpTestCaseParallelization(IEnumerable<IXunitTestCase> testCases)
{
var result = new List<IXunitTestCase>();
foreach (var testCase in testCases)
{
var oldTestMethod = testCase.TestMethod;
var oldTestClass = oldTestMethod.TestClass;
var oldTestCollection = oldTestMethod.TestClass.TestCollection;
// If the collection is explicitly set, don't try to parallelize test execution
if (oldTestCollection.CollectionDefinition != null || oldTestClass.Class.GetCustomAttributes(typeof(CollectionAttribute)).Any())
{
result.Add(testCase);
continue;
}
// Create a new collection with a unique id for the test case.
var newTestCollection =
new TestCollection(
oldTestCollection.TestAssembly,
oldTestCollection.CollectionDefinition,
displayName: $"{oldTestCollection.DisplayName} {oldTestCollection.UniqueID}");
newTestCollection.UniqueID = Guid.NewGuid();
// Duplicate the test and assign it to the new collection
var newTestClass = new TestClass(newTestCollection, oldTestClass.Class);
var newTestMethod = new TestMethod(newTestClass, oldTestMethod.Method);
switch (testCase)
{
// Used by Theory having DisableDiscoveryEnumeration or non-serializable data
case XunitTheoryTestCase xunitTheoryTestCase:
result.Add(new XunitTheoryTestCase(
DiagnosticMessageSink,
GetTestMethodDisplay(xunitTheoryTestCase),
GetTestMethodDisplayOptions(xunitTheoryTestCase),
newTestMethod));
break;
// Used by all other tests
case XunitTestCase xunitTestCase:
result.Add(new XunitTestCase(
DiagnosticMessageSink,
GetTestMethodDisplay(xunitTestCase),
GetTestMethodDisplayOptions(xunitTestCase),
newTestMethod,
xunitTestCase.TestMethodArguments));
break;
// TODO If you use custom attribute, you may need to add cases here
default:
throw new ArgumentOutOfRangeException("Test case " + testCase.GetType() + " not supported");
}
}
return result;
static TestMethodDisplay GetTestMethodDisplay(TestMethodTestCase testCase)
{
return (TestMethodDisplay)typeof(TestMethodTestCase)
.GetProperty("DefaultMethodDisplay", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(testCase)!;
}
static TestMethodDisplayOptions GetTestMethodDisplayOptions(TestMethodTestCase testCase)
{
return (TestMethodDisplayOptions)typeof(TestMethodTestCase)
.GetProperty("DefaultMethodDisplayOptions", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(testCase)!;
}
}
}
}
To instruct xUnit to use the custom TestFramework, apply the TestFrameworkAttribute in any file of the test project.
C#
[assembly: Xunit.TestFramework(typeName: "Xunit.Custom.ParallelTestFramework",
// TODO Use the Assembly Name of the project containing
// ParallelTestFramework instead of MyTestProject1
assemblyName: "MyTestProject1")]
The NuGet package Meziantou.Xunit.ParallelTestFramework contains the source code of ParallelTestFramework and adds the assembly attribute.
#Additional resources
Do you have a question or a suggestion about this post? Contact me!