diff --git a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs index c045ff9b9b..600e714327 100644 --- a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs +++ b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs @@ -174,6 +174,115 @@ private static void NotThrow(Exception exception, string because, object[] becau } } + /// + /// 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 async 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."); + } + + + 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; diff --git a/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs b/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs index 6320f73a08..c73ae00da1 100644 --- a/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs +++ b/Tests/Shared.Specs/AsyncFunctionExceptionAssertionSpecs.cs @@ -1,5 +1,7 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; +using FluentAssertions.Extensions; using Xunit; using Xunit.Sdk; @@ -26,7 +28,8 @@ public void When_subject_throws_subclass_of_expected_exact_exception_it_should_f // Assert //----------------------------------------------------------------------------------------------------------- action.Should().Throw() - .WithMessage("Expected type to be System.ArgumentException because IFoo.Do should do that, but found System.ArgumentNullException."); + .WithMessage( + "Expected type to be System.ArgumentException because IFoo.Do should do that, but found System.ArgumentNullException."); } [Fact] @@ -83,7 +86,6 @@ public async void When_async_method_throws_async_expected_exception_it_should_su // Assert //----------------------------------------------------------------------------------------------------------- await action.Should().ThrowAsync(); - } [Fact] @@ -105,7 +107,8 @@ public void When_async_method_does_not_throw_expected_exception_it_should_fail() // Assert //----------------------------------------------------------------------------------------------------------- action.Should().Throw() - .WithMessage("Expected System.InvalidOperationException because IFoo.Do should do that, but no exception was thrown*"); + .WithMessage( + "Expected System.InvalidOperationException because IFoo.Do should do that, but no exception was thrown*"); } [Fact] @@ -127,7 +130,8 @@ public void When_async_method_throws_unexpected_exception_it_should_fail() // Assert //----------------------------------------------------------------------------------------------------------- action.Should().Throw() - .WithMessage("Expected System.InvalidOperationException because IFoo.Do should do that, but found*System.ArgumentException*"); + .WithMessage( + "Expected System.InvalidOperationException because IFoo.Do should do that, but found*System.ArgumentException*"); } [Fact] @@ -236,7 +240,8 @@ public async Task When_subject_throws_subclass_of_expected_async_exact_exception //----------------------------------------------------------------------------------------------------------- // Assert //----------------------------------------------------------------------------------------------------------- - (await testAction.Should().ThrowAsync()).WithMessage("*ArgumentException*ABCDE*ArgumentNullException*"); + (await testAction.Should().ThrowAsync()).WithMessage( + "*ArgumentException*ABCDE*ArgumentNullException*"); } [Fact] @@ -540,6 +545,245 @@ public void When_asserting_async_void_method_should_not_throw_specific_exception //----------------------------------------------------------------------------------------------------------- action.Should().Throw("*async*void*"); } + + #region NotThrowAfter + + [Fact] + public void When_wait_time_is_negative_for_async_func_executed_with_wait_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var waitTime = -1.Milliseconds(); + var pollInterval = 10.Milliseconds(); + + var asyncObject = new AsyncClass(); + Func someFunc = async () => await asyncObject.SucceedAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => someFunc.Should().NotThrowAfter(waitTime, pollInterval); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw() + .WithMessage("* value of waitTime must be non-negative*"); + } + + [Fact] + public void When_poll_interval_is_negative_for_async_func_executed_with_wait_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var waitTime = 10.Milliseconds(); + var pollInterval = -1.Milliseconds(); + + var asyncObject = new AsyncClass(); + Func someFunc = async () => await asyncObject.SucceedAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => someFunc.Should().NotThrowAfter(waitTime, pollInterval); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw() + .WithMessage("* value of pollInterval must be non-negative*"); + } + + [Fact] + public void + When_no_exception_should_be_thrown_for_async_func_executed_with_wait_after_wait_time_but_it_was_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var watch = Stopwatch.StartNew(); + var waitTime = 100.Milliseconds(); + var pollInterval = 10.Milliseconds(); + + Func throwLongerThanWaitTime = async () => + { + if (watch.Elapsed <= waitTime + (waitTime.Milliseconds / 2).Milliseconds()) + { + throw new ArgumentException("An exception was forced"); + } + + await Task.Delay(0); + }; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action action = () => throwLongerThanWaitTime.Should() + .NotThrowAfter(waitTime, pollInterval, "we passed valid arguments"); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().Throw() + .WithMessage("Did not expect any exceptions after 0.100s because we passed valid arguments*"); + } + + [Fact] + public void When_no_exception_should_be_thrown_for_async_func_executed_with_wait_after_wait_time_and_none_was_it_should_not_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var watch = Stopwatch.StartNew(); + var waitTime = 100.Milliseconds(); + var pollInterval = 10.Milliseconds(); + + Func throwShorterThanWaitTime = async () => + { + if (watch.Elapsed <= (waitTime.Milliseconds / 2).Milliseconds()) + { + throw new ArgumentException("An exception was forced"); + } + await Task.Delay(0); + }; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + + Action act = () => throwShorterThanWaitTime.Should().NotThrowAfter(waitTime, pollInterval); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + + act.Should().NotThrow(); + } + + #endregion + + #region NotThrowAfterAsync + [Fact] + public void When_wait_time_is_negative_for_async_func_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var waitTime = -1.Milliseconds(); + var pollInterval = 10.Milliseconds(); + + var asyncObject = new AsyncClass(); + Func someFunc = async () => await asyncObject.SucceedAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func act = async () => + await someFunc.Should().NotThrowAfterAsync(waitTime, pollInterval); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw() + .WithMessage("* value of waitTime must be non-negative*"); + } + + [Fact] + public void When_poll_interval_is_negative_for_async_func_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var waitTime = 10.Milliseconds(); + var pollInterval = -1.Milliseconds(); + + var asyncObject = new AsyncClass(); + Func someFunc = async () => await asyncObject.SucceedAsync(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func act = async () => + await someFunc.Should().NotThrowAfterAsync(waitTime, pollInterval); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw() + .WithMessage("* value of pollInterval must be non-negative*"); + } + + [Fact] + public void When_no_exception_should_be_thrown_for_async_func_after_wait_time_but_it_was_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var watch = Stopwatch.StartNew(); + var waitTime = 100.Milliseconds(); + var pollInterval = 10.Milliseconds(); + + Func throwLongerThanWaitTime = async () => + { + if (watch.Elapsed <= waitTime + (waitTime.Milliseconds / 2).Milliseconds()) + { + throw new ArgumentException("An exception was forced"); + } + + await Task.Delay(0); + }; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => + await throwLongerThanWaitTime.Should() + .NotThrowAfterAsync(waitTime, pollInterval, "we passed valid arguments"); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().Throw() + .WithMessage("Did not expect any exceptions after 0.100s because we passed valid arguments*"); + } + + [Fact] + public void When_no_exception_should_be_thrown_for_async_func_after_wait_time_and_none_was_it_should_not_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var watch = Stopwatch.StartNew(); + var waitTime = 100.Milliseconds(); + var pollInterval = 10.Milliseconds(); + + Func throwShorterThanWaitTime = async () => + { + if (watch.Elapsed <= (waitTime.Milliseconds / 2).Milliseconds()) + { + throw new ArgumentException("An exception was forced"); + } + + await Task.Delay(0); + }; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + + Func act = async () => + await throwShorterThanWaitTime.Should().NotThrowAfterAsync(waitTime, pollInterval); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + + act.Should().NotThrow(); + } + + #endregion } internal class AsyncClass