diff --git a/Src/FluentAssertions/AssertionExtensions.Actions.cs b/Src/FluentAssertions/AssertionExtensions.Actions.cs index e8de6aa9e6..0faf1a50af 100644 --- a/Src/FluentAssertions/AssertionExtensions.Actions.cs +++ b/Src/FluentAssertions/AssertionExtensions.Actions.cs @@ -28,7 +28,9 @@ public static partial class AssertionExtensions /// /// Returns an object that allows asserting additional members of the thrown exception. /// - public static ExceptionAssertions ThrowExactly(this ActionAssertions actionAssertions, string because = "", + public static ExceptionAssertions ThrowExactly( + this ActionAssertions actionAssertions, + string because = "", params object[] becauseArgs) where TException : Exception { @@ -54,7 +56,9 @@ public static partial class AssertionExtensions /// /// Returns an object that allows asserting additional members of the thrown exception. /// - public static ExceptionAssertions ThrowExactly(this AsyncFunctionAssertions asyncActionAssertions, string because = "", + public static ExceptionAssertions ThrowExactly( + this AsyncActionAssertions asyncActionAssertions, + string because = "", params object[] becauseArgs) where TException : Exception { @@ -80,7 +84,9 @@ public static partial class AssertionExtensions /// /// Returns an object that allows asserting additional members of the thrown exception. /// - public static async Task> ThrowExactlyAsync(this AsyncFunctionAssertions asyncActionAssertions, string because = "", + public static async Task> ThrowExactlyAsync( + this AsyncActionAssertions asyncActionAssertions, + string because = "", params object[] becauseArgs) where TException : Exception { diff --git a/Src/FluentAssertions/AssertionExtensions.cs b/Src/FluentAssertions/AssertionExtensions.cs index 88b9f781e1..aa07823ae6 100644 --- a/Src/FluentAssertions/AssertionExtensions.cs +++ b/Src/FluentAssertions/AssertionExtensions.cs @@ -637,9 +637,9 @@ public static ActionAssertions Should(this Action action) /// current . /// [Pure] - public static AsyncFunctionAssertions Should(this Func action) + public static AsyncActionAssertions Should(this Func action) { - return new AsyncFunctionAssertions(action, extractor); + return new AsyncActionAssertions(action, extractor); } /// @@ -647,9 +647,9 @@ public static AsyncFunctionAssertions Should(this Func action) /// current System.Func{Task{T}}. /// [Pure] - public static AsyncFunctionAssertions Should(this Func> action) + public static AsyncFunctionAssertions Should(this Func> action) { - return new AsyncFunctionAssertions(action, extractor); + return new AsyncFunctionAssertions(action, extractor); } /// diff --git a/Src/FluentAssertions/Specialized/AsyncActionAssertions.cs b/Src/FluentAssertions/Specialized/AsyncActionAssertions.cs new file mode 100644 index 0000000000..e359ab7b9a --- /dev/null +++ b/Src/FluentAssertions/Specialized/AsyncActionAssertions.cs @@ -0,0 +1,361 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +using FluentAssertions.Execution; + +namespace FluentAssertions.Specialized +{ + /// + /// Contains a number of methods to assert that an asynchronous action method yields the expected result. + /// + [DebuggerNonUserCode] + public class AsyncActionAssertions + { + private readonly IExtractExceptions extractor; + + public AsyncActionAssertions(Func subject, IExtractExceptions extractor) + { + this.extractor = extractor; + Subject = subject; + } + + /// + /// Gets the that is being asserted. + /// + public Func Subject { get; private set; } + + /// + /// Asserts that the current throws an exception of type . + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public ExceptionAssertions Throw(string because = "", params object[] becauseArgs) + where TException : Exception + { + Exception exception = InvokeSubjectWithInterception(); + return Throw(exception, because, becauseArgs); + } + + /// + /// Asserts that the current throws an exception of type . + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public async Task> ThrowAsync(string because = "", params object[] becauseArgs) + where TException : Exception + { + Exception exception = await InvokeSubjectWithInterceptionAsync(); + return Throw(exception, because, becauseArgs); + } + + /// + /// Asserts that the current does not throw any exception. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public void NotThrow(string because = "", params object[] becauseArgs) + { + try + { + Task.Run(Subject).Wait(); + } + catch (Exception exception) + { + NotThrow(exception, because, becauseArgs); + } + } + + /// + /// Asserts that the current does not throw any exception. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public async Task NotThrowAsync(string because = "", params object[] becauseArgs) + { + try + { + await Task.Run(Subject); + } + catch (Exception exception) + { + NotThrow(exception, because, becauseArgs); + } + } + + /// + /// Asserts that the current does not throw an exception of type . + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public void NotThrow(string because = "", params object[] becauseArgs) + where TException : Exception + { + try + { + Task.Run(Subject).Wait(); + } + catch (Exception exception) + { + NotThrow(exception, because, becauseArgs); + } + } + + /// + /// Asserts that the current does not throw an exception of type . + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public async Task NotThrowAsync(string because = "", params object[] becauseArgs) + where TException : Exception + { + try + { + await Task.Run(Subject); + } + catch (Exception exception) + { + NotThrow(exception, because, becauseArgs); + } + } + + private static void NotThrow(Exception exception, string because, object[] becauseArgs) + { + Exception nonAggregateException = GetFirstNonAggregateException(exception); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect any exception{reason}, but found a {0} with message {1}.", + nonAggregateException.GetType(), nonAggregateException.ToString()); + } + + private static void NotThrow(Exception exception, string because, object[] becauseArgs) where TException : Exception + { + Exception nonAggregateException = GetFirstNonAggregateException(exception); + + if (nonAggregateException != null) + { + Execute.Assertion + .ForCondition(!(nonAggregateException is TException)) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect {0}{reason}, but found one with message {1}.", + typeof(TException), nonAggregateException.ToString()); + } + } + + /// + /// Asserts that the current stops throwing any exception + /// after a specified amount of time. + /// + /// + /// The is invoked. If it raises an exception, + /// the invocation is repeated until it either stops raising any exceptions + /// or the specified wait time is exceeded. + /// + /// + /// The time after which the should have stopped throwing any exception. + /// + /// + /// The time between subsequent invocations of the . + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Throws if waitTime or pollInterval are negative. + public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs) + { + if (waitTime < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(waitTime), $"The value of {nameof(waitTime)} must be non-negative."); + } + + if (pollInterval < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(pollInterval), $"The value of {nameof(pollInterval)} must be non-negative."); + } + + TimeSpan? invocationEndTime = null; + Exception exception = null; + var watch = Stopwatch.StartNew(); + + while (invocationEndTime is null || invocationEndTime < waitTime) + { + exception = InvokeSubjectWithInterception(); + if (exception is null) + { + return; + } + Task.Delay(pollInterval).Wait(); + invocationEndTime = watch.Elapsed; + } + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect any exceptions after {0}{reason}, but found {1}.", waitTime, exception); + } + + /// + /// Asserts that the current stops throwing any exception + /// after a specified amount of time. + /// + /// + /// The is invoked. If it raises an exception, + /// the invocation is repeated until it either stops raising any exceptions + /// or the specified wait time is exceeded. + /// + /// + /// The time after which the should have stopped throwing any exception. + /// + /// + /// The time between subsequent invocations of the . + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Throws if waitTime or pollInterval are negative. + public + Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs) + { + if (waitTime < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(waitTime), $"The value of {nameof(waitTime)} must be non-negative."); + } + + if (pollInterval < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(pollInterval), $"The value of {nameof(pollInterval)} must be non-negative."); + } + + return assertionTask(); + + async Task assertionTask() + { + TimeSpan? invocationEndTime = null; + Exception exception = null; + var watch = Stopwatch.StartNew(); + + while (invocationEndTime is null || invocationEndTime < waitTime) + { + exception = await InvokeSubjectWithInterceptionAsync(); + if (exception is null) + { + return; + } + + await Task.Delay(pollInterval); + invocationEndTime = watch.Elapsed; + } + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect any exceptions after {0}{reason}, but found {1}.", waitTime, exception); + } + } + + private static Exception GetFirstNonAggregateException(Exception exception) + { + Exception nonAggregateException = exception; + while (nonAggregateException is AggregateException) + { + nonAggregateException = nonAggregateException.InnerException; + } + + return nonAggregateException; + } + + private ExceptionAssertions Throw(Exception exception, string because, object[] becauseArgs) + where TException : Exception + { + var exceptions = extractor.OfType(exception); + + Execute.Assertion + .ForCondition(exception != null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {0}{reason}, but no exception was thrown.", typeof(TException)); + + Execute.Assertion + .ForCondition(exceptions.Any()) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {0}{reason}, but found {1}.", typeof(TException), exception); + + return new ExceptionAssertions(exceptions); + } + + private Exception InvokeSubjectWithInterception() + { + try + { + Task.Run(Subject).Wait(); + } + catch (Exception exception) + { + return InterceptException(exception); + } + + return null; + } + + private async Task InvokeSubjectWithInterceptionAsync() + { + try + { + await Task.Run(Subject); + } + catch (Exception exception) + { + return InterceptException(exception); + } + + return null; + } + + private Exception InterceptException(Exception exception) + { + var ar = exception as AggregateException; + if (ar?.InnerException is AggregateException) + { + return ar.InnerException; + } + + return exception; + } + } +} diff --git a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs index 019cb25c77..a4403e0e58 100644 --- a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs +++ b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs @@ -8,14 +8,14 @@ namespace FluentAssertions.Specialized { /// - /// Contains a number of methods to assert that an asynchronous method yields the expected result. + /// Contains a number of methods to assert that an asynchronous function yields the expected result. /// [DebuggerNonUserCode] - public class AsyncFunctionAssertions + public class AsyncFunctionAssertions { private readonly IExtractExceptions extractor; - public AsyncFunctionAssertions(Func subject, IExtractExceptions extractor) + public AsyncFunctionAssertions(Func> subject, IExtractExceptions extractor) { this.extractor = extractor; Subject = subject; @@ -24,7 +24,7 @@ public AsyncFunctionAssertions(Func subject, IExtractExceptions extractor) /// /// Gets the that is being asserted. /// - public Func Subject { get; private set; } + public Func> Subject { get; private set; } /// /// Asserts that the current throws an exception of type . @@ -70,15 +70,19 @@ public async Task> ThrowAsync(string /// /// Zero or more objects to format using the placeholders in . /// - public void NotThrow(string because = "", params object[] becauseArgs) + public AndWhichConstraint, T> NotThrow( + string because = "", + params object[] becauseArgs) { try { - Task.Run(Subject).Wait(); + T result = Task.Run(Subject).Result; + return new AndWhichConstraint, T>(this, result); } catch (Exception exception) { NotThrow(exception, because, becauseArgs); + return null; } } @@ -92,15 +96,17 @@ public void NotThrow(string because = "", params object[] becauseArgs) /// /// Zero or more objects to format using the placeholders in . /// - public async Task NotThrowAsync(string because = "", params object[] becauseArgs) + public async Task, T>> NotThrowAsync(string because = "", params object[] becauseArgs) { try { - await Task.Run(Subject); + T result = await Task.Run(Subject); + return new AndWhichConstraint, T>(this, result); } catch (Exception exception) { NotThrow(exception, because, becauseArgs); + return null; } } @@ -150,6 +156,59 @@ public async Task NotThrowAsync(string because = "", params object[] } } + /// + /// Asserts that the current throws the exact exception (and not a derived exception type). + /// + /// A reference to the method or property. + /// + /// The type of the exception it should throw. + /// + /// + /// A formatted phrase explaining why the assertion should be satisfied. If the phrase does not + /// start with the word because, it is prepended to the message. + /// + /// + /// Zero or more values to use for filling in any compatible placeholders. + /// + /// + /// Returns an object that allows asserting additional members of the thrown exception. + /// + public ExceptionAssertions ThrowExactly( + string because = "", + params object[] becauseArgs) + where TException : Exception + { + var exceptionAssertions = this.Throw(because, becauseArgs); + exceptionAssertions.Which.GetType().Should().Be(because, becauseArgs); + return exceptionAssertions; + } + + /// + /// Asserts that the current throws the exact exception (and not a derived exception type). + /// + /// + /// The type of the exception it should throw. + /// + /// + /// A formatted phrase explaining why the assertion should be satisfied. If the phrase does not + /// start with the word because, it is prepended to the message. + /// + /// + /// Zero or more values to use for filling in any compatible placeholders. + /// + /// + /// Returns an object that allows asserting additional members of the thrown exception. + /// + public async Task> ThrowExactlyAsync( + string because = "", + params object[] becauseArgs) + where TException : Exception + { + var exceptionAssertions = await this.ThrowAsync(because, becauseArgs); + exceptionAssertions.Which.GetType().Should().Be(because, becauseArgs); + return exceptionAssertions; + } + private static void NotThrow(Exception exception, string because, object[] becauseArgs) { Exception nonAggregateException = GetFirstNonAggregateException(exception); diff --git a/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs b/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs index b4b6154ca2..e7ba4fd039 100644 --- a/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs +++ b/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs @@ -205,6 +205,17 @@ public void When_function_of_task_int_in_async_method_throws_the_expected_except f.Should().Throw(); } + [Fact] + public async Task When_function_of_task_int_returns_value_result_can_be_asserted() + { + // Arrange/Act + Func> func = () => Task.FromResult(42); + + // Assert + func.Should().NotThrow().Which.Should().Be(42); + (await func.Should().NotThrowAsync()).Which.Should().Be(42); + } + [Fact] public void When_function_of_task_int_in_async_method_throws_not_excepted_exception_it_should_succeed() { @@ -221,7 +232,7 @@ public void When_function_of_task_int_in_async_method_throws_not_excepted_except } [Fact] - public async Task When_subject_throws_subclass_of_expected_async_exact_exception_it_should_throw() + public async Task When_action_throws_subclass_of_expected_async_exact_exception_it_should_throw() { //----------------------------------------------------------------------------------------------------------- // Arrange @@ -241,6 +252,27 @@ public async Task When_subject_throws_subclass_of_expected_async_exact_exception .WithMessage("*ArgumentException*ABCDE*ArgumentNullException*"); } + [Fact] + public async Task When_func_throws_subclass_of_expected_async_exact_exception_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var asyncObject = new AsyncClass(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func> action = async () => await asyncObject.ThrowTaskIntAsync(true); + Func testAction = async () => await action.Should().ThrowExactlyAsync("ABCDE"); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + (await testAction.Should().ThrowAsync()) + .WithMessage("*ArgumentException*ABCDE*ArgumentNullException*"); + } + [Fact] public async Task When_subject_throws_expected_async_exact_exception_it_should_succeed() { @@ -789,7 +821,7 @@ public async Task ThrowAsync() public async Task SucceedAsync() { - await Task.FromResult(0); + await Task.FromResult(42); } public Task IncompleteTask()