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()