From d0c5a9c698650d55ef1258aac570c192e0d95cc4 Mon Sep 17 00:00:00 2001 From: Gavin Lambert Date: Fri, 31 Mar 2023 20:04:03 +1300 Subject: [PATCH] ThatAsync and MultipleAsync --- .../framework/Assert.ThatAsync.cs | 84 ++++++++++++ src/NUnitFramework/framework/Assert.cs | 30 +++++ .../tests/Assertions/AssertMultipleTests.cs | 12 ++ .../tests/Assertions/AssertThatAsyncTests.cs | 123 ++++++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 src/NUnitFramework/framework/Assert.ThatAsync.cs create mode 100644 src/NUnitFramework/tests/Assertions/AssertThatAsyncTests.cs diff --git a/src/NUnitFramework/framework/Assert.ThatAsync.cs b/src/NUnitFramework/framework/Assert.ThatAsync.cs new file mode 100644 index 0000000000..2333aab3c8 --- /dev/null +++ b/src/NUnitFramework/framework/Assert.ThatAsync.cs @@ -0,0 +1,84 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +#nullable enable + +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using System; +using NUnit.Framework.Constraints; + +namespace NUnit.Framework +{ + public abstract partial class Assert + { + #region Assert.ThatAsync + + /// + /// Apply a constraint to an async delegate. Returns without throwing an exception when inside a multiple assert block. + /// + /// An AsyncTestDelegate to be executed + /// A Constraint expression to be applied + /// Awaitable. + public static Task ThatAsync(AsyncTestDelegate code, IResolveConstraint constraint) + { + return ThatAsync(code, constraint, null, null); + } + + /// + /// Apply a constraint to an async delegate. Returns without throwing an exception when inside a multiple assert block. + /// + /// An AsyncTestDelegate to be executed + /// A Constraint expression to be applied + /// The message that will be displayed on failure + /// Arguments to be used in formatting the message + /// Awaitable. + public static async Task ThatAsync(AsyncTestDelegate code, IResolveConstraint constraint, string? message, params object?[]? args) + { + try + { + await code(); + Assert.That(() => { }, constraint, message, args); + } + catch (Exception ex) + { + var edi = ExceptionDispatchInfo.Capture(ex); + Assert.That(() => edi.Throw(), constraint, message, args); + } + } + + /// + /// Apply a constraint to an async delegate. Returns without throwing an exception when inside a multiple assert block. + /// + /// An async method to be executed + /// A Constraint expression to be applied + /// Awaitable. + public static Task ThatAsync(Func> code, IResolveConstraint constraint) + { + return ThatAsync(code, constraint, null, null); + } + + /// + /// Apply a constraint to an async delegate. Returns without throwing an exception when inside a multiple assert block. + /// + /// An async method to be executed + /// A Constraint expression to be applied + /// The message that will be displayed on failure + /// Arguments to be used in formatting the message + /// Awaitable. + public static async Task ThatAsync(Func> code, IResolveConstraint constraint, string? message, params object?[]? args) + { + try + { + var result = await code(); + Assert.That(() => result, constraint, message, args); + } + catch (Exception ex) + { + var edi = ExceptionDispatchInfo.Capture(ex); + Assert.That(() => edi.Throw(), constraint, message, args); + } + } + + #endregion + } +} diff --git a/src/NUnitFramework/framework/Assert.cs b/src/NUnitFramework/framework/Assert.cs index 31290da6a8..b56b6ce0c7 100644 --- a/src/NUnitFramework/framework/Assert.cs +++ b/src/NUnitFramework/framework/Assert.cs @@ -6,6 +6,7 @@ using System.Collections; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using NUnit.Framework.Constraints; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; @@ -372,6 +373,35 @@ public static void Multiple(AsyncTestDelegate testDelegate) } } + /// + /// Wraps code containing a series of assertions, which should all + /// be executed, even if they fail. Failed results are saved and + /// reported at the end of the code block. + /// + /// An AsyncTestDelegate to be executed in Multiple Assertion mode. + public static async Task MultipleAsync(AsyncTestDelegate testDelegate) + { + TestExecutionContext context = TestExecutionContext.CurrentContext; + Guard.OperationValid(context != null, "There is no current test execution context."); + + context.MultipleAssertLevel++; + + try + { + await testDelegate(); + } + finally + { + context.MultipleAssertLevel--; + } + + if (context.MultipleAssertLevel == 0 && context.CurrentResult.PendingFailures > 0) + { + context.CurrentResult.RecordTestCompletion(); + throw new MultipleAssertException(context.CurrentResult); + } + } + #endregion #region Helper Methods diff --git a/src/NUnitFramework/tests/Assertions/AssertMultipleTests.cs b/src/NUnitFramework/tests/Assertions/AssertMultipleTests.cs index be02e9fa37..d3b04cb131 100644 --- a/src/NUnitFramework/tests/Assertions/AssertMultipleTests.cs +++ b/src/NUnitFramework/tests/Assertions/AssertMultipleTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt using System; +using System.Threading.Tasks; using NUnit.Framework.Interfaces; using NUnit.TestData.AssertMultipleData; using NUnit.TestUtilities; @@ -74,6 +75,17 @@ public void AssertMultiple_InvalidAssertThrowsException(string methodName, strin Assert.That(result.Message, Contains.Substring($"{invalidAssert} may not be used in a multiple assertion block.")); } + [Test] + public async Task AssertMultipleAsyncSucceeds() + { + await Assert.MultipleAsync(async () => + { + await Assert.ThatAsync(() => Task.FromResult(42), Is.EqualTo(42)); + Assert.That("hello", Is.EqualTo("hello")); + await Assert.ThatAsync(() => Task.FromException(new ArgumentNullException()), Throws.ArgumentNullException); + }); + } + private ITestResult CheckResult(string methodName, ResultState expectedResultState, int expectedAsserts, params string[] assertionMessageRegex) { ITestResult result = TestBuilder.RunTestCase(typeof(AssertMultipleFixture), methodName); diff --git a/src/NUnitFramework/tests/Assertions/AssertThatAsyncTests.cs b/src/NUnitFramework/tests/Assertions/AssertThatAsyncTests.cs new file mode 100644 index 0000000000..24b5ce1f77 --- /dev/null +++ b/src/NUnitFramework/tests/Assertions/AssertThatAsyncTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt + +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework.Internal; + +namespace NUnit.Framework.Assertions +{ + [TestFixture] + public class AssertThatAsyncTests + { + [Test] + public async Task AssertionPasses_CompletedTask_ThrowsNothing() + { + await Assert.ThatAsync(() => Task.CompletedTask, Throws.Nothing); + } + + [Test] + public async Task AssertionPasses_CanceledTask_ThrowsCanceled() + { + var cancel = new CancellationTokenSource(); + cancel.Cancel(); + + await Assert.ThatAsync(() => Task.FromCanceled(cancel.Token), + Throws.InstanceOf() + .With.Property(nameof(TaskCanceledException.CancellationToken)).EqualTo(cancel.Token)); + } + + [Test] + public async Task AssertionPasses_FaultedTask_ThrowsMatchingException() + { + await Assert.ThatAsync(() => Task.FromException(new InvalidOperationException()), Throws.InvalidOperationException); + } + + [Test] + public async Task AssertionPasses_CompletedTask_ThrowsNothingWithMessage() + { + await Assert.ThatAsync(() => Task.CompletedTask, Throws.Nothing, "Success"); + } + + [Test] + public async Task AssertionPasses_CanceledTask_ThrowsCanceledWithMessage() + { + var cancel = new CancellationTokenSource(); + cancel.Cancel(); + + await Assert.ThatAsync(() => Task.FromCanceled(cancel.Token), + Throws.InstanceOf() + .With.Property(nameof(TaskCanceledException.CancellationToken)).EqualTo(cancel.Token), + "Cancelled"); + } + + [Test] + public async Task AssertionPasses_FaultedTask_ThrowsMatchingExceptionWithMessage() + { + await Assert.ThatAsync(() => Task.FromException(new InvalidOperationException()), Throws.InvalidOperationException, "Faulted"); + } + + [Test] + public async Task Failure_CompletedTask_ThrowsException() + { + await AssertAssertionFailsAsync(async () => await Assert.ThatAsync(() => Task.CompletedTask, Throws.InvalidOperationException)); + } + + [Test] + public async Task Failure_CanceledTask_ThrowsNothing() + { + var cancel = new CancellationTokenSource(); + cancel.Cancel(); + + await AssertAssertionFailsAsync(async () => await Assert.ThatAsync(() => Task.FromCanceled(cancel.Token), Throws.Nothing)); + } + + [Test] + public async Task Failure_FaultedTask_ThrowsNothing() + { + await AssertAssertionFailsAsync(async () => await Assert.ThatAsync(() => Task.FromException(new InvalidOperationException()), Throws.Nothing)); + } + + [Test] + public async Task AssertionPasses_CompletedTaskWithResult_ThrowsNothing() + { + await Assert.ThatAsync(() => Task.FromResult(42), Throws.Nothing); + } + + [Test] + public async Task AssertionPasses_CompletedTaskWithResult_EqualsResult() + { + await Assert.ThatAsync(() => Task.FromResult(42), Is.EqualTo(42)); + } + + [Test] + public async Task AssertionPasses_CanceledTaskWithResult_ThrowsCanceled() + { + var cancel = new CancellationTokenSource(); + cancel.Cancel(); + + await Assert.ThatAsync(() => Task.FromCanceled(cancel.Token), + Throws.InstanceOf() + .With.Property(nameof(TaskCanceledException.CancellationToken)).EqualTo(cancel.Token)); + } + + [Test] + public async Task AssertionPasses_FaultedTaskWithResult_ThrowsMatchingException() + { + await Assert.ThatAsync(() => Task.FromException(new InvalidOperationException()), Throws.InvalidOperationException); + } + + private static async Task AssertAssertionFailsAsync(Func assertion) + { + await Assert.ThatAsync( + async () => + { + using (new TestExecutionContext.IsolatedContext()) + { + await assertion(); + } + }, + Throws.InstanceOf()); + } + } +}