Core Framework v3 2.0.0 2025 March 1
Today, we're shipping one new release:
- xUnit.net Core Framework v3
2.0.0
It's been 3 weeks since the release of 1.1.0
.
As always, we'd like to thank all the users who contributed to the success of xUnit.net through usage, feedback, and code. 🎉
Release Notes
These release notes are a list of changes from 1.1.0
to 2.0.0
.
This release contains breaking changes as indicated by the major version bump. Binary compatibility with 1.x.y
packages is not guaranteed, and extensibility projects should verify whether these breaking changes affect them as they may be required to issue new versions.
Target Framework and Dependency Updates
The minimum version of .NET has been bumped up from .NET 6 to .NET 8 as Microsoft discontinued support for .NET 6 in November 2024. The .NET Framework minimum version remains at
4.7.2
.The minimum version of
Microsoft.Testing.Platform
has been upgraded to1.6.2
.The minimum version of
System.Collections.Immutable
has been downgraded to6.0.0
.The minimum version of
System.Memory
has been downgraded to4.5.5
.
Core Framework
[Fact]
has added a new property:/// <summary> /// Gets exceptions that, when thrown, will cause the test to be skipped rather than failed. /// </summary> /// <remarks> /// The skip reason will be the exception's message. /// </remarks> public Type[]? SkipExceptions { get; set; }
This property allows developers to indicate which exception types, when thrown, should be considered a skipped test rather than a failed test. The expected common usage here will be for exceptions like
NotSupportedException
which indicate that the test cannot run in the current execution environment. For more information, see the linked issue. xunit/xunit#3101The following breaking changes were caused by this change:
- Adding
IFactAttribute.SkipExceptions
. - Adding
FactAttribute.SkipExceptions
. - Adding
IXunitTestCase.SkipExceptions
. - Adding
skipExceptions
parameter to theXunitTestCase
constructor. - Adding
skipExceptions
parameter to theXunitDelayEnumeratedTheoryTestCase
constructor. - Adding
SkipExceptions
to the return tuple fromTestIntrospectionHelper.GetTestCaseDetails
and.GetTestCaseDetailsForTheoryDataRow
. - Updated logic in
XunitTestRunnerBaseContext.GetSkipReason
to support the skip exception types.
- Adding
We have updated an old design for cancellation, wherein test execution cancellation could only be requested in response to a message being sent by the execution engine. This design dates back to v1/v2 wherein the cross-
AppDomain
communication created limitations in communication between the runner and the execution engine. The primary downside is that the developer would have to wait for a message from the execution engine before the cancellation request could take place. If the system was stuck waiting for long-running tests, that meant that the cancellation request would be delayed until at least one of the long-running tests finished (success or failure).With the v3 in-process runner design, we can now use
CancellationToken
, in concert with the newTestContext.Current.CancellationToken
to immediately propagate cancellation requests without waiting for an execution engine message. In practice, this means that long-running tests will cancel immediately when requested (assuming the unit test is using the cancellation token from the test context for long-running operations). xunit/xunit#3122The following breaking changes were caused by this change:
InProcessController
methodsFind
,FindAndRun
, andRun
methods now all takeCancellationTokenSource
.ITestFrameworkExecution.RunTestCases
now takes an optionalCancellationToken
.TestFrameworkExecutor
's implementation ofRunTestsCases
will propagate this the cancellation token, for extensibility authors who derive from this base class.- The
TestAssemblyRunnerContext
constructor now takes a requiredCancellationToken
, and rather than manufacturing a newCancellationTokenSource
(as it did in1.x.y
), it will instead wrap the cancellation token. Developers overriding theCancellationTokenSource
property should be careful not to discard the original token source, since this would also discard the original token. - The constructors for
XunitTestAssemblyRunnerBaseContext
andXunitTestAssemblyRunnerContext
also require aCancellationToken
, since they derive fromTestAssemblyRunnerContext
. XunitTestAssemblyRunner.Run
now takes a requiredCancellationToken
, so that it can construct the context correctly.- The
ProjectAssemblyRunner
constructor now takes a requiredCancellationTokenSource
. (xunit.v3.runner.inproc.console
) ConsoleRunner
has becomeIDisposable
in order to dispose of the cancellation token source that it creates. (xunit.v3.runner.inproc.console
)
Retrieving attachments when generating execution messages has been updated to improve extensibility.
The following breaking changes were caused by this change:
TestRunnerBase
has a new extensibility point,ValueTask<IReadOnlyDictionary<string, TestAttachment>?> GetAttachments(TContext ctxt)
, which is used to retrieve the attachments for the test finished message. By default, this returnsnull
.TestRunnerBase.OnTestFinished
has an additional parameter,IReadOnlyDictionary<string, TestAttachment>? attachments
TestRunnerBase.Run
has been updated to callGetAttachments
and pass the value toOnTestFinished
. This call is made immediately after the call toGetTestOutput
andGetWarnings
after the test has finished executing.XunitTestRunnerBase.GetAttachments
is now whereTestContext.Current.Attachments
is called. Previously, this call lived inTestRunnerBase.OnTestFinished
, so the result of this shift is that extensibility code that relied onTestRunnerBase
to get attachments from the test context will need to be updated to overrideGetAttachments
just likeXunitTestRunnerBase
. (This also allows extensibility authors to choose a non-TestContext
based source for attachments.)
BUG: We have fixed an issue where there was ambiguity with overloads of
Record.ExceptionAsync
that took bothTask
andValueTask
overloads. We have removed theValueTask
overloads. If you were previously calling aValueTask
overload, you can call.AsTask()
on theValueTask
instance to turn it into aTask
. (We had previous fixed the ambiguity in async assertions, likeAssert.ThrowsAsync
, butRecord.ExceptionAsync
was overlooked when the original change was implemented.) xunit/xunit#2808BUG: We unintentionally reordered the calling order of
BeforeAfterTestAttribute
attributes. Assembly attributes are called first, then test collection attributes, then test class attributes, then test method attributes. This restores the ordering behavior from v2. xunit/xunit#3180
Assertion Library
We have updated the assertion failure display for
Assert.Equal
when there is a failure comparing strings inside of containers. xunit/xunit#3126Consider this example test:
[Fact] public void TestMethod() { var expected = new[] { @"C:\Program Files (x86)\Common Files\Extremely Long Path Name\VST2", }; var actual = new[] { @"C:\Program Files (x86)\Common Files\Extremely Long Path Name\VST3" }; Assert.Equal(expected, actual); }
Previously when strings were not equal inside of containers, those strings were printed with the standard "first
n
characters" formatting, which made it harder for users to understand what the failed comparison was (especially with long strings):TestClass.TestMethod [FAIL] Assert.Equal() Failure: Collections differ ↓ (pos 0) Expected: ["C:\\Program Files (x86)\\Common Files\\Extremely L"···] Actual: ["C:\\Program Files (x86)\\Common Files\\Extremely L"···] ↑ (pos 0)
Now in 2.0.0, this prints a more useful message which highlights the string comparsion, using the index pointers for string failure points and using a single index notation on which item in the collection caused the failure:
TestClass.TestMethod [FAIL] Assert.Equal() Failure: Collections differ at index 0 ↓ (pos 64) Expected: ···"s (x86)\\Common Files\\Extremely Long Path Name\\VST2" Actual: ···"s (x86)\\Common Files\\Extremely Long Path Name\\VST3" ↑ (pos 64)
We have added
IAssertEqualityComparer<T>
, which is implemented byAssertEqualityComparer<T>
. This new interface has a single method attached to it:AssertEqualityResult Equals(T? x, CollectionTracker? xTracker, T? y, CollectionTracker? yTracker);
This replaces the older version of this method on
AssertEqualityComparer<T>
, which returnedbool
. The new method returnsAssertEqualityResult
, which is defined as:public class AssertEqualityResult : IEquatable<AssertEqualityResult> { /// <summary> /// Returns <c>true</c> if the values were equal; <c>false</c>, otherwise. /// </summary> public bool Equal { get; } /// <summary> /// Returns the exception that caused the failure, if it was based on an exception. /// </summary> public Exception? Exception { get; } /// <summary> /// Returns the comparer result for any inner comparison that caused this result /// to fail; returns <c>null</c> if there was no inner comparison. /// </summary> /// <remarks> /// If this value is set, then it generally indicates that this comparison was a /// failed collection comparison, and the inner result indicates the specific /// item comparison that caused the failure. /// </remarks> public AssertEqualityResult? InnerResult { get; } /// <summary> /// Returns the index of the mismatch for the <c>X</c> value, if the comparison /// failed on a specific index. /// </summary> public int? MismatchIndexX { get; } /// <summary> /// Returns the index of the mismatch for the <c>Y</c> value, if the comparison /// failed on a specific index. /// </summary> public int? MismatchIndexY { get; } /// <summary> /// The left-hand value in the comparison /// </summary> public object? X { get; } /// <summary> /// The right-hand value in the comparison /// </summary> public object? Y { get; } }
This new data structure captures information about the comparison results, including the ability to dive into inner comparison results. This allows for richer potential output messages for assertion failures.
Additionally, code that previous relied on the class
AssertEqualityComparer<T>
(which is internal) can now rely on both first and third party implementations ofIAssertEqualityComparer<T>
(in particular,Assert.Equal
andAssert.NotEqual
), which allows third parties to write comparers which can participate in the richer result messages.We have added
StringAssertEqualityComparer
which centralizes the previous string comparison logic that was an implementation detail ofAssert.Equal
andAssert.NotEqual
withstring
(andchar
-base span) overloads. The primary entry point are two static functions:public static AssertEqualityResult Equivalent( string? expected, string? actual, bool ignoreCase = false, bool ignoreLineEndingDifferences = false, bool ignoreWhiteSpaceDifferences = false, bool ignoreAllWhiteSpace = false); public static AssertEqualityResult Equivalent( ReadOnlySpan<char> expected, ReadOnlySpan<char> actual, bool ignoreCase = false, bool ignoreLineEndingDifferences = false, bool ignoreWhiteSpaceDifferences = false, bool ignoreAllWhiteSpace = false);
You'll note that we've opted to call these functions
Equivalent
rather thanEqual
given their ability to customize their comparisons with respect to case, line endings, and white space. These flags are equivalent to thestring
(andchar
-based span) overloads ofAssert.Equal
andAssert.NotEqual
.A new factory function overload for
EqualException
has been added for string mismatches with a custom header:public static EqualException ForMismatchedStringsWithHeader( string? expected, string? actual, int expectedIndex, int actualIndex, string header);
The primary comparison function on
CollectionTracker
was also updated with the new data structure. This method:public static bool AreCollectionsEqual( CollectionTracker? x, CollectionTracker? y, IEqualityComparer itemComparer, bool isDefaultItemComparer, out int? mismatchedIndex);
has been replaced by this method:
public static AssertEqualityResult AreCollectionsEqual( CollectionTracker? x, CollectionTracker? y, IEqualityComparer itemComparer, bool isDefaultItemComparer);
In addition to returning
AssertEqualityResult
rather thanbool
, there is another subtle but important change. Previously this function would not catch exceptions when third party comparers would throw, but rather relied on ensuringmismatchedIndex
was set appropriately when those exceptions were propagated. The new code catches all exceptions, and returnsAssertEqualityResult
objects where.Exception
has the captured exception.The old method was marked with
[Obsolete]
and will be removed in3.0.0
.Two new types,
AssertEquivalenceComparer
(implementingIEqualityComparer
) andAssertEquivalenceComparer<T>
(implementingIEqualityComparer<T>
) have been added. These callAssert.Equivalent
on the passed values, including throwingEqualivalentException
when the comparison fails rather than returningfalse
. The constructor for these types allow you to provide the value forstrict
that would normally be passed toAssert.Equivalent
. xunit/xunit#3186The primary intended usage for this is in response to a request for an overload of
Assert.Equivalent
for collections which required the collection values be in the exact order. When considering potential ways to satisfy this request, these new types were provided which can be passed toAssert.Equal
(which guarantees identical order of collections) and passing this type for the item comparer:public sealed record class Foo(int Id, string Name) { public bool Equals(Foo? other) => Id == other?.Id; public override int GetHashCode() => Id.GetHashCode(); } public class UnitTest { [Fact] public void Collection_of_foo_should_be_ordered_equivalent() { var fooOrg = new Foo(1, "foo"); var fooUpdated = fooOrg with { Name = "bar" }; var actual = new[] { fooUpdated, fooOrg }; var expected = new[] { fooOrg, fooUpdated }; Assert.Equal(expected, actual, new AssertEquivalenceComparer<Foo>(strict: false)); } }
The failure for this looks like:
UnitTest.Collection_of_foo_should_be_ordered_equivalent [FAIL] Assert.Equal() Failure: Exception thrown during comparison ↓ (pos 0) Expected: [Foo { Id = 1, Name = foo }, Foo { Id = 1, Name = bar }] Actual: [Foo { Id = 1, Name = bar }, Foo { Id = 1, Name = foo }] ↑ (pos 0) ---- Assert.Equivalent() Failure: Mismatched value on member 'Name' Expected: "foo" Actual: "bar"
Project Templates
We have updated the projects templates (shipped in
xunit.v3.templates
) to allow overriden target frameworks via--framework
(or-f
):xunit3
allowsnet8.0
(default),net9.0
,net472
,net48
, andnet481
.xunit3-extension
allowsnetstandard2.0
(default),net8.0
,net9.0
,net472
,net48
, andnet481
.
The target framework override is also available when using the new templates inside Visual Studio and JetBrains Rider.