Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-generic overload for WithInnerExceptionExactly #1769

Merged
merged 9 commits into from Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 44 additions & 0 deletions Src/FluentAssertions/ExceptionAssertionsExtensions.cs
Expand Up @@ -80,6 +80,28 @@ public static class ExceptionAssertionsExtensions
return (await task).WithInnerException<TInnerException>(because, becauseArgs);
}

/// <summary>
/// Asserts that the thrown exception contains an inner exception of type <param name="innerException" />.
/// </summary>
/// <typeparam name="TException">The expected type of the exception.</typeparam>
/// <param name="task">The <see cref="ExceptionAssertions{TException}"/> containing the thrown exception.</param>
/// <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 <paramref name="because" />.
/// </param>
public static async Task<ExceptionAssertions<Exception>> WithInnerException<TException>(
this Task<ExceptionAssertions<TException>> task,
Type innerException,
string because = "",
params object[] becauseArgs)
where TException : Exception
{
return (await task).WithInnerException(innerException, because, becauseArgs);
}

/// <summary>
/// Asserts that the thrown exception contains an inner exception of the exact type <typeparamref name="TInnerException" /> (and not a derived exception type).
/// </summary>
Expand All @@ -103,6 +125,28 @@ public static class ExceptionAssertionsExtensions
return (await task).WithInnerExceptionExactly<TInnerException>(because, becauseArgs);
}

/// <summary>
/// Asserts that the thrown exception contains an inner exception of the exact type <param name="innerException" /> (and not a derived exception type).
/// </summary>
/// <typeparam name="TException">The expected type of the exception.</typeparam>
/// <param name="task">The <see cref="ExceptionAssertions{TException}"/> containing the thrown exception.</param>
/// <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 <paramref name="because" />.
/// </param>
public static async Task<ExceptionAssertions<Exception>> WithInnerExceptionExactly<TException>(
this Task<ExceptionAssertions<TException>> task,
Type innerException,
string because = "",
params object[] becauseArgs)
where TException : Exception
{
return (await task).WithInnerExceptionExactly(innerException, because, becauseArgs);
}

/// <summary>
/// Asserts that the thrown exception has a parameter which name matches <paramref name="paramName" />.
/// </summary>
Expand Down
120 changes: 84 additions & 36 deletions Src/FluentAssertions/Specialized/ExceptionAssertions.cs
Expand Up @@ -3,9 +3,7 @@
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;

using FluentAssertions.Common;
using FluentAssertions.Equivalency;
using FluentAssertions.Equivalency.Steps;
using FluentAssertions.Execution;
using FluentAssertions.Formatting;
Expand Down Expand Up @@ -89,7 +87,8 @@ public ExceptionAssertions(IEnumerable<TException> exceptions)
.ForCondition(Subject.Any())
.FailWith("Expected exception with message {0}{reason}, but no exception was thrown.", expectedWildcardPattern);

OuterMessageAssertion.Execute(Subject.Select(exc => exc.Message).ToArray(), expectedWildcardPattern, because, becauseArgs);
OuterMessageAssertion.Execute(Subject.Select(exc => exc.Message).ToArray(), expectedWildcardPattern, because,
becauseArgs);

return this;
}
Expand All @@ -109,28 +108,24 @@ public ExceptionAssertions(IEnumerable<TException> exceptions)
params object[] becauseArgs)
where TInnerException : Exception
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected inner {0}{reason}, but ", typeof(TInnerException))
.ForCondition(Subject is not null)
.FailWith("no exception was thrown.")
.Then
.ForCondition(Subject.Any(e => e.InnerException is not null))
.FailWith("the thrown exception has no inner exception.")
.Then
.ClearExpectation();

TInnerException[] expectedInnerExceptions = Subject
.Select(e => e.InnerException)
.OfType<TInnerException>()
.ToArray();

Execute.Assertion
.ForCondition(expectedInnerExceptions.Any())
.BecauseOf(because, becauseArgs)
.FailWith("Expected inner {0}{reason}, but found {1}.", typeof(TInnerException), SingleSubject.InnerException);
var expectedInnerExceptions = AssertInnerExceptions(typeof(TInnerException), because, becauseArgs);
return new ExceptionAssertions<TInnerException>(expectedInnerExceptions.Cast<TInnerException>());
}

return new ExceptionAssertions<TInnerException>(expectedInnerExceptions);
/// <summary>
/// Asserts that the thrown exception contains an inner exception of type <param name="innerException" />.
/// </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 <paramref name="because" />.
/// </param>
public virtual ExceptionAssertions<Exception> WithInnerException(Type innerException, string because = null,
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
params object[] becauseArgs)
{
return new ExceptionAssertions<Exception>(AssertInnerExceptions(innerException, because, becauseArgs));
}

/// <summary>
Expand All @@ -148,19 +143,24 @@ public ExceptionAssertions(IEnumerable<TException> exceptions)
params object[] becauseArgs)
where TInnerException : Exception
{
WithInnerException<TInnerException>(because, becauseArgs);

TInnerException[] expectedExceptions = Subject
.Select(e => e.InnerException)
.OfType<TInnerException>()
.Where(e => e?.GetType() == typeof(TInnerException)).ToArray();

Execute.Assertion
.ForCondition(expectedExceptions.Any())
.BecauseOf(because, becauseArgs)
.FailWith("Expected inner {0}{reason}, but found {1}.", typeof(TInnerException), SingleSubject.InnerException);
var exceptionExpression = AssertInnerExceptionExactly(typeof(TInnerException), because, becauseArgs);
return new ExceptionAssertions<TInnerException>(exceptionExpression.Cast<TInnerException>());
}

return new ExceptionAssertions<TInnerException>(expectedExceptions);
/// <summary>
/// Asserts that the thrown exception contains an inner exception of the exact type <param name="innerException" /> (and not a derived exception type).
/// </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 <paramref name="because" />.
/// </param>
public virtual ExceptionAssertions<Exception> WithInnerExceptionExactly(Type innerException, string because = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove virtual here as well.
As a rule-of-thumb, when we review PRs we only point out a single occurrence of something that should be changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thank you for the Patience. It is my first time contributing to anything.

params object[] becauseArgs)
{
return new ExceptionAssertions<Exception>(AssertInnerExceptionExactly(innerException, because, becauseArgs));
}

/// <summary>
Expand Down Expand Up @@ -191,6 +191,54 @@ public ExceptionAssertions(IEnumerable<TException> exceptions)
return this;
}

private IEnumerable<Exception> AssertInnerExceptionExactly(Type innerException, string because = null,
params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(innerException, nameof(innerException));

AssertInnerExceptions(innerException, because, becauseArgs);

Exception[] expectedExceptions = Subject
.Select(e => e.InnerException)
.Where(e => e?.GetType() == innerException).ToArray();

Execute.Assertion
.ForCondition(expectedExceptions.Any())
.BecauseOf(because, becauseArgs)
.FailWith("Expected inner {0}{reason}, but found {1}.", innerException, SingleSubject.InnerException);

return expectedExceptions;
}

private IEnumerable<Exception> AssertInnerExceptions(Type innerException, string because = null,
params object[] becauseArgs)
{
Guard.ThrowIfArgumentIsNull(innerException, nameof(innerException));

Execute.Assertion
.BecauseOf(because, becauseArgs)
.WithExpectation("Expected inner {0}{reason}, but ", innerException)
.ForCondition(Subject is not null)
.FailWith("no exception was thrown.")
.Then
.ForCondition(Subject.Any(e => e.InnerException is not null))
.FailWith("the thrown exception has no inner exception.")
.Then
.ClearExpectation();

Exception[] expectedInnerExceptions = Subject
.Select(e => e.InnerException)
.Where(e => e?.GetType() == innerException)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ This checks that the runtime type of e is exactly innerException.
This breaks checking derived types.

We need a test for this scenario.
You could copy When_subject_throws_an_exception_with_the_expected_inner_exception_it_should_not_do_anything and change the inner exception to a derived one, e.g. ArgumentNullException.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I missed that and thought this code merely moved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is one of the few changes, so that it would work for both call types. Unfortunately I did not fully grasp the full extent of the OfType<> method. Will fix it!

.ToArray();

Execute.Assertion
.ForCondition(expectedInnerExceptions.Any())
.BecauseOf(because, becauseArgs)
.FailWith("Expected inner {0}{reason}, but found {1}.", innerException, SingleSubject.InnerException);

return expectedInnerExceptions;
}

private TException SingleSubject
{
get
Expand Down
Expand Up @@ -832,6 +832,21 @@ public async Task When_async_method_throws_the_expected_inner_exception_it_shoul
await action.Should().NotThrowAsync();
}

[Fact]
public async Task When_async_method_throws_the_expected_inner_exception_from_argument_it_should_succeed()
{
// Arrange
Func<Task> task = () => Throw.Async(new AggregateException(new InvalidOperationException()));

// Act
Func<Task> action = () => task
.Should().ThrowAsync<AggregateException>()
.WithInnerException(typeof(InvalidOperationException));

// Assert
await action.Should().NotThrowAsync();
jnyrup marked this conversation as resolved.
Show resolved Hide resolved
}

[Fact]
public async Task When_async_method_throws_the_expected_inner_exception_exactly_it_should_succeed()
{
Expand All @@ -847,6 +862,21 @@ public async Task When_async_method_throws_the_expected_inner_exception_exactly_
await action.Should().NotThrowAsync();
}

[Fact]
public async Task When_async_method_throws_the_expected_inner_exception_exactly_defined_in_arguments_it_should_succeed()
{
// Arrange
Func<Task> task = () => Throw.Async(new AggregateException(new ArgumentException()));

// Act
Func<Task> action = () => task
.Should().ThrowAsync<AggregateException>()
.WithInnerExceptionExactly(typeof(ArgumentException));

// Assert
await action.Should().NotThrowAsync();
}

[Fact]
public async Task When_async_method_throws_aggregate_exception_containing_expected_exception_it_should_succeed()
{
Expand Down Expand Up @@ -890,6 +920,21 @@ public async Task When_async_method_does_not_throw_the_expected_inner_exception_
await action.Should().ThrowAsync<XunitException>().WithMessage("*InvalidOperation*Argument*");
}

[Fact]
public async Task When_async_method_does_not_throw_the_expected_inner_exception_from_argument_it_should_fail()
{
// Arrange
Func<Task> task = () => Throw.Async(new AggregateException(new ArgumentException()));

// Act
Func<Task> action = () => task
.Should().ThrowAsync<AggregateException>()
.WithInnerException(typeof(InvalidOperationException));

// Assert
await action.Should().ThrowAsync<XunitException>().WithMessage("*InvalidOperation*Argument*");
}

[Fact]
public async Task When_async_method_does_not_throw_the_expected_inner_exception_exactly_it_should_fail()
{
Expand All @@ -905,6 +950,22 @@ public async Task When_async_method_does_not_throw_the_expected_inner_exception_
await action.Should().ThrowAsync<XunitException>().WithMessage("*ArgumentException*ArgumentNullException*");
}

[Fact]
public async Task
When_async_method_does_not_throw_the_expected_inner_exception_exactly_defined_in_arguments_it_should_fail()
{
// Arrange
Func<Task> task = () => Throw.Async(new AggregateException(new ArgumentNullException()));

// Act
Func<Task> action = () => task
.Should().ThrowAsync<AggregateException>()
.WithInnerExceptionExactly(typeof(ArgumentException));

// Assert
await action.Should().ThrowAsync<XunitException>().WithMessage("*ArgumentException*ArgumentNullException*");
}

[Fact]
public async Task When_async_method_does_not_throw_the_expected_exception_it_should_fail()
{
Expand Down