/// <summary>
/// Provides ordered test assertions.
/// </summary>
/// <remarks>
/// For more detail, see “How to Order xUnit Tests and Collections” by Tom DuPont
/// [http://www.tomdupont.net/2016/04/how-to-order-xunit-tests-and-collections.html]
/// </remarks>
[TestCaseOrderer(TestCaseOrderer.TypeName, TestCaseOrderer.AssemblyName)]
public abstract class OrderedTestBase
{
/// <summary>
/// Initializes a new instance of the <see cref="OrderedTestBase"/> class.
/// </summary>
public OrderedTestBase()
{
AppDomain.CurrentDomain.FirstChanceException += (source, e) =>
{
if (xUnitExceptionHasOccurred || !isXunitException(e.Exception)) return;
xUnitExceptionHasOccurred = true;
lastException = e.Exception;
};
}
/// <summary>
/// The expected ordinal of the current test.
/// </summary>
protected static int TestOrdinal;
/// <summary>
/// Asserts there are no exceptions of type <c>xUnit.Sdk.*</c>
/// to prevent ordered tests from running.
/// </summary>
/// <remarks>
/// See https://github.com/xunit/xunit/issues/856
/// </remarks>
protected static void AssertNoXUnitException() =>
Assert.False(xUnitExceptionHasOccurred, $"Assertion Failed: An exception has occurred under test [message: `{lastException?.Message}`].");
/// <summary>
/// Asserts the name of the test.
/// </summary>
/// <param name="testName">Name of the test.</param>
protected void AssertTestName([CallerMemberName] string testName = "")
{
var type = GetType();
Assert.False(string.IsNullOrEmpty(type.FullName));
var queue = TestCaseOrderer.QueuedTests[type.FullName];
var result = queue.TryDequeue(out string? dequeuedName);
Assert.True(result);
Assert.Equal(testName, dequeuedName);
}
protected static bool xUnitExceptionHasOccurred;
static Exception? lastException;
/// <summary>
/// Determines whether the specified <see cref="Exception"/> is from an xUnit assertion.
/// </summary>
/// <param name="ex">The ex.</param>
/// <returns>
/// <c>true</c> if it is a xUnit <see cref="Exception"/>; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// <see cref="AppDomain.FirstChanceException"/> will detect ALL exceptions,
/// including those NOT on the current path of execution!
///
/// This means a huge amount of exceptions can pass through <see cref="AppDomain.FirstChanceException"/>.
/// </remarks>
bool isXunitException(Exception ex) =>
ex?.GetType().FullName?.ToLowerInvariant().StartsWith("xunit.sdk") == true;
}
/// <summary>
/// Implementation of <see cref="ITestCaseOrderer"/>
/// for the use of <see cref="TestOrderAttribute"/>.
/// </summary>
/// <seealso cref="ITestCaseOrderer" />
/// <remarks>
/// For more detail, see “How to Order xUnit Tests and Collections” by Tom DuPont.
/// [ see http://www.tomdupont.net/2016/04/how-to-order-xunit-tests-and-collections.html ]
/// </remarks>
public class TestCaseOrderer : ITestCaseOrderer
{
/// <summary>The queued tests</summary>
public static readonly ConcurrentDictionary<string, ConcurrentQueue<string>> QueuedTests = new();
/// <summary>Orders test cases for execution.</summary>
/// <typeparam name="TTestCase"></typeparam>
/// <param name="testCases">The test cases to be ordered.</param>
/// <returns>The test cases in the order to be run.</returns>
public IEnumerable<TTestCase> OrderTestCases<TTestCase>(IEnumerable<TTestCase> testCases)
where TTestCase : ITestCase
{
TTestCase[] orderedCases = testCases.OrderBy(GetOrder).ToArray();
return orderedCases;
}
static int GetOrder<TTestCase>(TTestCase testCase) where TTestCase : ITestCase
{
// Enqueue the test name.
QueuedTests
.GetOrAdd(testCase.TestMethod.TestClass.Class.Name, _ => new ConcurrentQueue<string>())
.Enqueue(testCase.TestMethod.Method.Name);
// Order the test based on the attribute.
var attr = testCase.TestMethod.Method
.ToRuntimeMethod()
.GetCustomAttribute<TestOrderAttribute>();
return attr?.Ordinal ?? 0;
}
}
/// <summary>
/// Defines the attribute used to order tests
/// by the specified ordinal.
/// </summary>
/// <seealso cref="Attribute" />
public class TestOrderAttribute : Attribute
{
/// <summary>Initializes a new instance of the <see cref="TestOrderAttribute"/> class.</summary>
/// <param name="ordinal">The ordinal.</param>
public TestOrderAttribute(int ordinal) => Ordinal = ordinal;
/// <summary>Initializes a new instance of the <see cref="TestOrderAttribute"/> class.</summary>
/// <param name="ordinal">The ordinal.</param>
/// <param name="reason">The reason.</param>
public TestOrderAttribute(int ordinal, string? reason)
{
Ordinal = ordinal;
Reason = reason;
}
/// <summary>Gets the ordinal.</summary>
/// <value>The ordinal.</value>
public int Ordinal { get; }
/// <summary>Gets the reason for choosing the order.</summary>
/// <value>The reason.</value>
public string? Reason { get; }
}
SonghayCore.OrderedTests
project 🚜 ✨