From 4982935dc5b4ed57967f1dfbe213f98f2bccae7a Mon Sep 17 00:00:00 2001 From: Artur Krajewski Date: Mon, 1 Oct 2018 07:28:43 +0200 Subject: [PATCH] Add `ThrowAsync`, `ThrowExactlyAsync` and `NotThrowAsync` (#931) * Add `ThrowAsync` and appropriate test * Add `NotThrowAsync` and appropriate tests * Document `ThrowAsync` and `NotThrowAsync` * Add `ThrowExactlyAsync` and appropriate tests --- .../AssertionExtensions.Actions.cs | 26 +++ .../Specialized/AsyncFunctionAssertions.cs | 171 ++++++++++++++---- .../AsyncFunctionExceptionAssertionSpecs.cs | 117 ++++++++++++ docs/_pages/documentation.md | 2 + 4 files changed, 277 insertions(+), 39 deletions(-) diff --git a/Src/FluentAssertions/AssertionExtensions.Actions.cs b/Src/FluentAssertions/AssertionExtensions.Actions.cs index 1258b18ae1..8b33fd771b 100644 --- a/Src/FluentAssertions/AssertionExtensions.Actions.cs +++ b/Src/FluentAssertions/AssertionExtensions.Actions.cs @@ -64,6 +64,32 @@ public static partial class AssertionExtensions return exceptionAssertions; } + /// + /// Asserts that the subject 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 static async Task> ThrowExactlyAsync(this AsyncFunctionAssertions asyncActionAssertions, string because = "", + params object[] becauseArgs) + where TException : Exception + { + var exceptionAssertions = await asyncActionAssertions.ThrowAsync(because, becauseArgs); + exceptionAssertions.Which.GetType().Should().Be(because, becauseArgs); + return exceptionAssertions; + } + private class AggregateExceptionExtractor : IExtractExceptions { public IEnumerable OfType(Exception actualException) diff --git a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs index 65a2ae3fc7..c045ff9b9b 100644 --- a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs +++ b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs @@ -40,19 +40,24 @@ public ExceptionAssertions Throw(string because = "", pa where TException : Exception { Exception exception = InvokeSubjectWithInterception(); - 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 Throw(exception, because, becauseArgs); + } - return new ExceptionAssertions(exceptions); + /// + /// 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); } /// @@ -73,12 +78,29 @@ public void NotThrow(string because = "", params object[] becauseArgs) } catch (Exception exception) { - Exception nonAggregateException = GetFirstNonAggregateException(exception); + NotThrow(exception, because, becauseArgs); + } + } - Execute.Assertion - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect any exception{reason}, but found a {0} with message {1}.", - nonAggregateException.GetType(), nonAggregateException.ToString()); + /// + /// 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); } } @@ -101,16 +123,54 @@ public void NotThrow(string because = "", params object[] becauseArg } catch (Exception 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()); - } + 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()); } } @@ -125,28 +185,61 @@ private static Exception GetFirstNonAggregateException(Exception exception) return nonAggregateException; } - private Exception InvokeSubjectWithInterception() + private ExceptionAssertions Throw(Exception exception, string because, object[] becauseArgs) + where TException : Exception { - Exception actualException = null; + 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) { - var ar = exception as AggregateException; - if (ar?.InnerException is AggregateException) - { - actualException = ar.InnerException; - } - else - { - actualException = 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 actualException; + return exception; } } } diff --git a/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs b/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs index 3d5ac85572..09ed56561e 100644 --- a/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs +++ b/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs @@ -67,6 +67,26 @@ public void When_async_method_throws_expected_exception_it_should_succeed() action.Should().NotThrow(); } + [Fact] + public async void When_async_method_throws_async_expected_exception_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var asyncObject = new AsyncClass(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => await asyncObject.ThrowAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + await action.Should().ThrowAsync(); + + } + [Fact] public void When_async_method_does_not_throw_expected_exception_it_should_fail() { @@ -132,6 +152,83 @@ public void When_async_method_does_not_throw_exception_and_that_was_expected_it_ action.Should().NotThrow(); } + [Fact] + public async Task When_async_method_does_not_throw_async_exception_and_that_was_expected_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var asyncObject = new AsyncClass(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => await asyncObject.SucceedAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + await action.Should().NotThrowAsync(); + } + + [Fact] + public async Task When_subject_throws_subclass_of_expected_async_exception_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var asyncObject = new AsyncClass(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => await asyncObject.ThrowAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + await action.Should().ThrowAsync("because {0} should do that", "IFoo.Do"); + } + + [Fact] + public async Task When_subject_throws_subclass_of_expected_async_exact_exception_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var asyncObject = new AsyncClass(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => await asyncObject.ThrowAsync(); + 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() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var asyncObject = new AsyncClass(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => await asyncObject.ThrowAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + await action.Should().ThrowExactlyAsync("because {0} should do that", "IFoo.Do"); + } + [Fact] public void When_async_method_throws_exception_and_no_exception_was_expected_it_should_fail() { @@ -175,6 +272,26 @@ public void When_async_method_throws_exception_and_expected_not_to_throw_another action.Should().NotThrow(); } + [Fact] + public async Task When_async_method_throws_exception_and_expected_not_to_throw_async_another_one_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var asyncObject = new AsyncClass(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => await asyncObject.ThrowAsync(); + + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + await action.Should().NotThrowAsync(); + } + [Fact] public void When_async_method_succeeds_and_expected_not_to_throw_particular_exception_it_should_succeed() { diff --git a/docs/_pages/documentation.md b/docs/_pages/documentation.md index 080dced9e9..9f873b0edc 100644 --- a/docs/_pages/documentation.md +++ b/docs/_pages/documentation.md @@ -714,6 +714,8 @@ Talking about the `async` keyword, you can also verify that an asynchronously ex ```csharp Func act = async () => { await asyncObject.ThrowAsync(); }; +await act.Should().ThrowAsync(); +await act.Should().NotThrowAsync(); act.Should().Throw(); act.Should().NotThrow(); ```