Skip to content

Commit

Permalink
Add ThrowAsync, ThrowExactlyAsync and NotThrowAsync (#931)
Browse files Browse the repository at this point in the history
* Add `ThrowAsync` and appropriate test

* Add `NotThrowAsync` and appropriate tests

* Document `ThrowAsync` and `NotThrowAsync`

* Add `ThrowExactlyAsync` and appropriate tests
  • Loading branch information
krajek authored and jnyrup committed Oct 1, 2018
1 parent 71d4d9b commit 4982935
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 39 deletions.
26 changes: 26 additions & 0 deletions Src/FluentAssertions/AssertionExtensions.Actions.cs
Expand Up @@ -64,6 +64,32 @@ public static partial class AssertionExtensions
return exceptionAssertions;
}

/// <summary>
/// Asserts that the <paramref name="asyncActionAssertions"/> subject throws the exact exception (and not a derived exception type).
/// </summary>
/// <param name="asyncActionAssertions">A reference to the method or property.</param>
/// <typeparam name="TException">
/// The type of the exception it should throw.
/// </typeparam>
/// <param name="because">
/// A formatted phrase explaining why the assertion should be satisfied. If the phrase does not
/// start with the word <i>because</i>, it is prepended to the message.
/// </param>
/// <param name="becauseArgs">
/// Zero or more values to use for filling in any <see cref="string.Format(string,object[])"/> compatible placeholders.
/// </param>
/// <returns>
/// Returns an object that allows asserting additional members of the thrown exception.
/// </returns>
public static async Task<ExceptionAssertions<TException>> ThrowExactlyAsync<TException>(this AsyncFunctionAssertions asyncActionAssertions, string because = "",
params object[] becauseArgs)
where TException : Exception
{
var exceptionAssertions = await asyncActionAssertions.ThrowAsync<TException>(because, becauseArgs);
exceptionAssertions.Which.GetType().Should().Be<TException>(because, becauseArgs);
return exceptionAssertions;
}

private class AggregateExceptionExtractor : IExtractExceptions
{
public IEnumerable<T> OfType<T>(Exception actualException)
Expand Down
171 changes: 132 additions & 39 deletions Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs
Expand Up @@ -40,19 +40,24 @@ public ExceptionAssertions<TException> Throw<TException>(string because = "", pa
where TException : Exception
{
Exception exception = InvokeSubjectWithInterception();
var exceptions = extractor.OfType<TException>(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<TException>(exception, because, becauseArgs);
}

return new ExceptionAssertions<TException>(exceptions);
/// <summary>
/// Asserts that the current <see cref="Func{Task}"/> throws an exception of type <typeparamref name="TException"/>.
/// </summary>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public async Task<ExceptionAssertions<TException>> ThrowAsync<TException>(string because = "", params object[] becauseArgs)
where TException : Exception
{
Exception exception = await InvokeSubjectWithInterceptionAsync();
return Throw<TException>(exception, because, becauseArgs);
}

/// <summary>
Expand All @@ -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());
/// <summary>
/// Asserts that the current <see cref="Func{Task}"/> does not throw any exception.
/// </summary>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public async Task NotThrowAsync(string because = "", params object[] becauseArgs)
{
try
{
await Task.Run(Subject);
}
catch (Exception exception)
{
NotThrow(exception, because, becauseArgs);
}
}

Expand All @@ -101,16 +123,54 @@ public void NotThrow<TException>(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<TException>(exception, because, becauseArgs);
}
}

/// <summary>
/// Asserts that the current <see cref="Func{Task}"/> does not throw an exception of type <typeparamref name="TException"/>.
/// </summary>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <see cref="because" />.
/// </param>
public async Task NotThrowAsync<TException>(string because = "", params object[] becauseArgs)
where TException : Exception
{
try
{
await Task.Run(Subject);
}
catch (Exception exception)
{
NotThrow<TException>(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<TException>(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());
}
}

Expand All @@ -125,28 +185,61 @@ private static Exception GetFirstNonAggregateException(Exception exception)
return nonAggregateException;
}

private Exception InvokeSubjectWithInterception()
private ExceptionAssertions<TException> Throw<TException>(Exception exception, string because, object[] becauseArgs)
where TException : Exception
{
Exception actualException = null;
var exceptions = extractor.OfType<TException>(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<TException>(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<Exception> 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;
}
}
}
117 changes: 117 additions & 0 deletions Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs
Expand Up @@ -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<Task> action = async () => await asyncObject.ThrowAsync<ArgumentException>();

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
await action.Should().ThrowAsync<ArgumentException>();

}

[Fact]
public void When_async_method_does_not_throw_expected_exception_it_should_fail()
{
Expand Down Expand Up @@ -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<Task> 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<Task> action = async () => await asyncObject.ThrowAsync<ArgumentNullException>();

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
await action.Should().ThrowAsync<ArgumentException>("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<Task> action = async () => await asyncObject.ThrowAsync<ArgumentNullException>();
Func<Task> testAction = async () => await action.Should().ThrowExactlyAsync<ArgumentException>("ABCDE");

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
(await testAction.Should().ThrowAsync<XunitException>()).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<Task> action = async () => await asyncObject.ThrowAsync<ArgumentException>();

//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
await action.Should().ThrowExactlyAsync<ArgumentException>("because {0} should do that", "IFoo.Do");
}

[Fact]
public void When_async_method_throws_exception_and_no_exception_was_expected_it_should_fail()
{
Expand Down Expand Up @@ -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<Task> action = async () => await asyncObject.ThrowAsync<ArgumentException>();


//-----------------------------------------------------------------------------------------------------------
// Assert
//-----------------------------------------------------------------------------------------------------------
await action.Should().NotThrowAsync<InvalidOperationException>();
}

[Fact]
public void When_async_method_succeeds_and_expected_not_to_throw_particular_exception_it_should_succeed()
{
Expand Down
2 changes: 2 additions & 0 deletions docs/_pages/documentation.md
Expand Up @@ -714,6 +714,8 @@ Talking about the `async` keyword, you can also verify that an asynchronously ex

```csharp
Func<Task> act = async () => { await asyncObject.ThrowAsync<ArgumentException>(); };
await act.Should().ThrowAsync<InvalidOperationException>();
await act.Should().NotThrowAsync();
act.Should().Throw<InvalidOperationException>();
act.Should().NotThrow();
```
Expand Down

0 comments on commit 4982935

Please sign in to comment.