From eae4a19ac40179cb7be06c16dcca11d26f4f8e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Gr=C3=BCtzmacher?= Date: Tue, 19 Mar 2019 22:22:24 +0100 Subject: [PATCH] introduced assertions for Task and Task with CompleteWithin checks introduce ITimer to abstract real timing in the tests rework after review rework after review Another pass of review. --- .../AggregateExceptionExtractor.cs | 41 ++++++ .../AssertionExtensions.Actions.cs | 38 ------ Src/FluentAssertions/AssertionExtensions.cs | 12 +- Src/FluentAssertions/Common/ITimer.cs | 29 +++++ Src/FluentAssertions/Common/TaskTimer.cs | 25 ++++ .../Specialized/AsyncFunctionAssertions.cs | 22 ++-- .../GenericAsyncFunctionAssertions.cs | 81 ++++++++++++ .../NonGenericAsyncFunctionAssertions.cs | 79 +++++++++++ Tests/Shared.Specs/AssertionExtensions.cs | 26 ++++ Tests/Shared.Specs/Shared.Specs.projitems | 4 + Tests/Shared.Specs/TaskAssertionSpecs.cs | 97 ++++++++++++++ Tests/Shared.Specs/TaskOfTAssertionSpecs.cs | 123 ++++++++++++++++++ Tests/Shared.Specs/TestingTimer.cs | 36 +++++ 13 files changed, 562 insertions(+), 51 deletions(-) create mode 100644 Src/FluentAssertions/AggregateExceptionExtractor.cs create mode 100644 Src/FluentAssertions/Common/ITimer.cs create mode 100644 Src/FluentAssertions/Common/TaskTimer.cs create mode 100644 Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs create mode 100644 Src/FluentAssertions/Specialized/NonGenericAsyncFunctionAssertions.cs create mode 100644 Tests/Shared.Specs/AssertionExtensions.cs create mode 100644 Tests/Shared.Specs/TaskAssertionSpecs.cs create mode 100644 Tests/Shared.Specs/TaskOfTAssertionSpecs.cs create mode 100644 Tests/Shared.Specs/TestingTimer.cs diff --git a/Src/FluentAssertions/AggregateExceptionExtractor.cs b/Src/FluentAssertions/AggregateExceptionExtractor.cs new file mode 100644 index 0000000000..fc004e2198 --- /dev/null +++ b/Src/FluentAssertions/AggregateExceptionExtractor.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions.Common; +using FluentAssertions.Specialized; + +namespace FluentAssertions +{ + public class AggregateExceptionExtractor : IExtractExceptions + { + public IEnumerable OfType(Exception actualException) + where T : Exception + { + if (typeof(T).IsSameOrInherits(typeof(AggregateException))) + { + return (actualException is T exception) ? new[] { exception } : Enumerable.Empty(); + } + + return GetExtractedExceptions(actualException); + } + + private static List GetExtractedExceptions(Exception actualException) + where T : Exception + { + var exceptions = new List(); + + if (actualException is AggregateException aggregateException) + { + var flattenedExceptions = aggregateException.Flatten(); + + exceptions.AddRange(flattenedExceptions.InnerExceptions.OfType()); + } + else if (actualException is T genericException) + { + exceptions.Add(genericException); + } + + return exceptions; + } + } +} \ No newline at end of file diff --git a/Src/FluentAssertions/AssertionExtensions.Actions.cs b/Src/FluentAssertions/AssertionExtensions.Actions.cs index e8de6aa9e6..6f90ccf5ee 100644 --- a/Src/FluentAssertions/AssertionExtensions.Actions.cs +++ b/Src/FluentAssertions/AssertionExtensions.Actions.cs @@ -1,16 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using FluentAssertions.Common; using FluentAssertions.Specialized; namespace FluentAssertions { public static partial class AssertionExtensions { - private static readonly AggregateExceptionExtractor extractor = new AggregateExceptionExtractor(); - /// /// Asserts that the subject throws the exact exception (and not a derived exception type). /// @@ -88,38 +83,5 @@ public static partial class AssertionExtensions exceptionAssertions.Which.GetType().Should().Be(because, becauseArgs); return exceptionAssertions; } - - private class AggregateExceptionExtractor : IExtractExceptions - { - public IEnumerable OfType(Exception actualException) - where T : Exception - { - if (typeof(T).IsSameOrInherits(typeof(AggregateException))) - { - return (actualException is T exception) ? new[] { exception } : Enumerable.Empty(); - } - - return GetExtractedExceptions(actualException); - } - - private static List GetExtractedExceptions(Exception actualException) - where T : Exception - { - var exceptions = new List(); - - if (actualException is AggregateException aggregateException) - { - var flattenedExceptions = aggregateException.Flatten(); - - exceptions.AddRange(flattenedExceptions.InnerExceptions.OfType()); - } - else if (actualException is T genericException) - { - exceptions.Add(genericException); - } - - return exceptions; - } - } } } diff --git a/Src/FluentAssertions/AssertionExtensions.cs b/Src/FluentAssertions/AssertionExtensions.cs index 6df824a9f2..7848ea8e35 100644 --- a/Src/FluentAssertions/AssertionExtensions.cs +++ b/Src/FluentAssertions/AssertionExtensions.cs @@ -25,6 +25,8 @@ namespace FluentAssertions [DebuggerNonUserCode] public static partial class AssertionExtensions { + private static readonly AggregateExceptionExtractor extractor = new AggregateExceptionExtractor(); + /// /// Invokes the specified action on an subject so that you can chain it with any of the ShouldThrow or ShouldNotThrow /// overloads. @@ -638,9 +640,9 @@ public static ActionAssertions Should(this Action action) /// current . /// [Pure] - public static AsyncFunctionAssertions Should(this Func action) + public static NonGenericAsyncFunctionAssertions Should(this Func action) { - return new AsyncFunctionAssertions(action.ExecuteInDefaultSynchronizationContext, extractor); + return new NonGenericAsyncFunctionAssertions(action.ExecuteInDefaultSynchronizationContext, extractor, new TaskTimer()); } /// @@ -648,9 +650,9 @@ public static AsyncFunctionAssertions Should(this Func action) /// current System.Func{Task{T}}. /// [Pure] - public static AsyncFunctionAssertions Should(this Func> action) + public static GenericAsyncFunctionAssertions Should(this Func> action) { - return new AsyncFunctionAssertions(action.ExecuteInDefaultSynchronizationContext, extractor); + return new GenericAsyncFunctionAssertions(action.ExecuteInDefaultSynchronizationContext, extractor, new TaskTimer()); } /// @@ -662,7 +664,7 @@ public static FunctionAssertions Should(this Func func) { return new FunctionAssertions(func, extractor); } - + #if NET45 || NET47 || NETCOREAPP2_0 diff --git a/Src/FluentAssertions/Common/ITimer.cs b/Src/FluentAssertions/Common/ITimer.cs new file mode 100644 index 0000000000..3e7b27e77f --- /dev/null +++ b/Src/FluentAssertions/Common/ITimer.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FluentAssertions.Common +{ + /// + /// Represents an abstract timer that is used to make some of this library's timing dependent functionality better testable. + /// + public interface ITimer + { + /// + /// Creates a task that will complete after a time delay. + /// + /// The time span to wait before completing the returned task + /// + /// A task that represents the time delay. + /// + Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken); + + /// + /// Waits for the task for a specified time. + /// + /// The task to be waited for. + /// The time span to wait. + /// true if the task completes before specified timeout. + bool Wait(Task task, TimeSpan timeout); + } +} diff --git a/Src/FluentAssertions/Common/TaskTimer.cs b/Src/FluentAssertions/Common/TaskTimer.cs new file mode 100644 index 0000000000..4f71d75449 --- /dev/null +++ b/Src/FluentAssertions/Common/TaskTimer.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FluentAssertions.Common +{ + /// + /// Default implementation for for production use. + /// + internal class TaskTimer : ITimer + { + public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) + { + return Task.Delay(delay, cancellationToken); + } + + public bool Wait(Task task, TimeSpan timeout) + { + using (NoSynchronizationContextScope.Enter()) + { + return task.Wait(timeout); + } + } + } +} diff --git a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs index 8b0191588a..d220d55ecb 100644 --- a/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs +++ b/Src/FluentAssertions/Specialized/AsyncFunctionAssertions.cs @@ -10,11 +10,11 @@ namespace FluentAssertions.Specialized /// Contains a number of methods to assert that an asynchronous method yields the expected result. /// [DebuggerNonUserCode] - public class AsyncFunctionAssertions + public abstract class AsyncFunctionAssertions { private readonly IExtractExceptions extractor; - public AsyncFunctionAssertions(Func subject, IExtractExceptions extractor) + protected AsyncFunctionAssertions(Func subject, IExtractExceptions extractor) { this.extractor = extractor; Subject = subject; @@ -52,7 +52,8 @@ public ExceptionAssertions Throw(string because = "", pa /// /// Zero or more objects to format using the placeholders in . /// - public async Task> ThrowAsync(string because = "", params object[] becauseArgs) + public async Task> ThrowAsync(string because = "", + params object[] becauseArgs) where TException : Exception { Exception exception = await InvokeSubjectWithInterceptionAsync(); @@ -159,7 +160,8 @@ private static void NotThrow(Exception exception, string because, object[] becau nonAggregateException.GetType(), nonAggregateException.ToString()); } - private static void NotThrow(Exception exception, string because, object[] becauseArgs) where TException : Exception + private static void NotThrow(Exception exception, string because, object[] becauseArgs) + where TException : Exception { Exception nonAggregateException = GetFirstNonAggregateException(exception); @@ -205,7 +207,8 @@ public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string becau if (pollInterval < TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(pollInterval), $"The value of {nameof(pollInterval)} must be non-negative."); + throw new ArgumentOutOfRangeException(nameof(pollInterval), + $"The value of {nameof(pollInterval)} must be non-negative."); } TimeSpan? invocationEndTime = null; @@ -219,9 +222,11 @@ public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string becau { 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); @@ -251,7 +256,7 @@ public void NotThrowAfter(TimeSpan waitTime, TimeSpan pollInterval, string becau /// /// Throws if waitTime or pollInterval are negative. public - Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs) + Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because = "", params object[] becauseArgs) { if (waitTime < TimeSpan.Zero) { @@ -260,7 +265,8 @@ Task NotThrowAfterAsync(TimeSpan waitTime, TimeSpan pollInterval, string because if (pollInterval < TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(pollInterval), $"The value of {nameof(pollInterval)} must be non-negative."); + throw new ArgumentOutOfRangeException(nameof(pollInterval), + $"The value of {nameof(pollInterval)} must be non-negative."); } return assertionTask(); @@ -303,7 +309,7 @@ private static Exception GetFirstNonAggregateException(Exception exception) private ExceptionAssertions Throw(Exception exception, string because, object[] becauseArgs) where TException : Exception { - var exceptions = extractor.OfType(exception); + var exceptions = extractor.OfType(exception).ToArray(); Execute.Assertion .ForCondition(exception != null) diff --git a/Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs b/Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs new file mode 100644 index 0000000000..f87400fbe6 --- /dev/null +++ b/Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions.Common; +using FluentAssertions.Execution; + +namespace FluentAssertions.Specialized +{ + public class GenericAsyncFunctionAssertions : AsyncFunctionAssertions + { + private readonly Func> subject; + private readonly ITimer timer; + + public GenericAsyncFunctionAssertions(Func> subject, IExtractExceptions extractor, ITimer timer) : base(subject, extractor) + { + this.subject = subject; + this.timer = timer; + } + + /// + /// Asserts that the current will complete within specified time. + /// + /// The allowed time span for the operation. + /// + /// 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 AndWhichConstraint, Task> CompleteWithin( + TimeSpan timeSpan, string because = "", params object[] becauseArgs) + { + Task task = subject(); + bool completed = timer.Wait(task, timeSpan); + + Execute.Assertion + .ForCondition(completed) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan); + + return new AndWhichConstraint, Task>(this, task); + } + + /// + /// Asserts that the current will complete within the specified time. + /// + /// The allowed time span for the operation. + /// + /// 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, Task>> CompleteWithinAsync( + TimeSpan timeSpan, string because = "", params object[] becauseArgs) + { + var timeoutCancellationTokenSource = new CancellationTokenSource(); + + Task task = subject(); + + Task completedTask = + await Task.WhenAny(task, timer.DelayAsync(timeSpan, timeoutCancellationTokenSource.Token)); + + if (completedTask == task) + { + timeoutCancellationTokenSource.Cancel(); + await completedTask; + } + + Execute.Assertion + .ForCondition(completedTask == task) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan); + + return new AndWhichConstraint, Task>(this, task); + } + + } +} diff --git a/Src/FluentAssertions/Specialized/NonGenericAsyncFunctionAssertions.cs b/Src/FluentAssertions/Specialized/NonGenericAsyncFunctionAssertions.cs new file mode 100644 index 0000000000..b24297d407 --- /dev/null +++ b/Src/FluentAssertions/Specialized/NonGenericAsyncFunctionAssertions.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions.Common; +using FluentAssertions.Execution; + +namespace FluentAssertions.Specialized +{ + public class NonGenericAsyncFunctionAssertions : AsyncFunctionAssertions + { + private readonly ITimer timer; + + public NonGenericAsyncFunctionAssertions(Func subject, IExtractExceptions extractor, ITimer timer) : base(subject, + extractor) + { + this.timer = timer; + } + + /// + /// Asserts that the current will complete within specified time. + /// + /// The allowed time span for the operation. + /// + /// 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 AndWhichConstraint CompleteWithin( + TimeSpan timeSpan, string because = "", params object[] becauseArgs) + { + Task task = Subject(); + bool completed = timer.Wait(task, timeSpan); + + Execute.Assertion + .ForCondition(completed) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan); + + return new AndWhichConstraint(this, task); + } + + /// + /// Asserts that the current will complete within the specified time. + /// + /// The allowed time span for the operation. + /// + /// 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> CompleteWithinAsync( + TimeSpan timeSpan, string because = "", params object[] becauseArgs) + { + var timeoutCancellationTokenSource = new CancellationTokenSource(); + + Task task = Subject(); + + Task completedTask = + await Task.WhenAny(task, timer.DelayAsync(timeSpan, timeoutCancellationTokenSource.Token)); + + if (completedTask == task) + { + timeoutCancellationTokenSource.Cancel(); + await completedTask; + } + + Execute.Assertion + .ForCondition(completedTask == task) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:task} to complete within {0}{reason}.", timeSpan); + + return new AndWhichConstraint(this, task); + } + } +} diff --git a/Tests/Shared.Specs/AssertionExtensions.cs b/Tests/Shared.Specs/AssertionExtensions.cs new file mode 100644 index 0000000000..34b59bde86 --- /dev/null +++ b/Tests/Shared.Specs/AssertionExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions.Common; +using FluentAssertions.Specialized; + +namespace FluentAssertions.Specs +{ + internal static class AssertionExtensions + { + private static readonly AggregateExceptionExtractor extractor = new AggregateExceptionExtractor(); + + public static NonGenericAsyncFunctionAssertions Should(this Func action, ITimer timer) + { + return new NonGenericAsyncFunctionAssertions(action, extractor, timer); + } + + /// + /// Returns a object that can be used to assert the + /// current System.Func{Task{T}}. + /// + public static GenericAsyncFunctionAssertions Should(this Func> action, ITimer timer) + { + return new GenericAsyncFunctionAssertions(action, extractor, timer); + } + } +} diff --git a/Tests/Shared.Specs/Shared.Specs.projitems b/Tests/Shared.Specs/Shared.Specs.projitems index 82417589a3..5b7569e14d 100644 --- a/Tests/Shared.Specs/Shared.Specs.projitems +++ b/Tests/Shared.Specs/Shared.Specs.projitems @@ -12,6 +12,7 @@ + @@ -60,6 +61,9 @@ + + + diff --git a/Tests/Shared.Specs/TaskAssertionSpecs.cs b/Tests/Shared.Specs/TaskAssertionSpecs.cs new file mode 100644 index 0000000000..f858e2be48 --- /dev/null +++ b/Tests/Shared.Specs/TaskAssertionSpecs.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions.Extensions; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Specs +{ + public class TaskAssertionSpecs + { + [Fact] + public void When_task_completes_fast_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action action = () => taskFactory.Awaiting(t => t.Task).Should(timer).CompleteWithin(100.Milliseconds()); + taskFactory.SetResult(true); + timer.CompletesBeforeTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().NotThrow(); + } + + [Fact] + public void When_task_completes_slow_it_should_fail() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action action = () => taskFactory.Awaiting(t => t.Task).Should(timer).CompleteWithin(100.Milliseconds()); + timer.RunsIntoTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().Throw(); + } + + [Fact] + public void When_task_completes_fast_async_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = () => taskFactory.Awaiting(t => t.Task).Should(timer).CompleteWithinAsync(100.Milliseconds()); + taskFactory.SetResult(true); + timer.CompletesBeforeTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().NotThrow(); + } + + [Fact] + public void When_task_completes_slow_async_it_should_fail() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = () => taskFactory.Awaiting(t => t.Task).Should(timer).CompleteWithinAsync(100.Milliseconds()); + timer.RunsIntoTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().Throw(); + } + } +} diff --git a/Tests/Shared.Specs/TaskOfTAssertionSpecs.cs b/Tests/Shared.Specs/TaskOfTAssertionSpecs.cs new file mode 100644 index 0000000000..36f00a6a51 --- /dev/null +++ b/Tests/Shared.Specs/TaskOfTAssertionSpecs.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions.Extensions; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Specs +{ + public class TaskOfTAssertionSpecs + { + [Fact] + public void When_task_completes_fast_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action action = () => + { + Func> func = () => taskFactory.Task; + + func.Should(timer).CompleteWithin(100.Milliseconds()) + .Which.Result.Should().Be(42); + }; + + taskFactory.SetResult(42); + timer.CompletesBeforeTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().NotThrow(); + } + + [Fact] + public void When_task_completes_slow_it_should_fail() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action action = () => + { + Func> func = () => taskFactory.Task; + + func.Should(timer).CompleteWithin(100.Milliseconds()); + }; + + timer.RunsIntoTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().Throw(); + } + + [Fact] + public void When_task_completes_fast_async_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = async () => + { + Func> func = () => taskFactory.Task; + + (await func.Should(timer).CompleteWithinAsync(100.Milliseconds())) + .Which.Result.Should().Be(42); + }; + + taskFactory.SetResult(42); + timer.CompletesBeforeTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().NotThrow(); + } + + [Fact] + public void When_task_completes_slow_async_it_should_fail() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var timer = new TestingTimer(); + var taskFactory = new TaskCompletionSource(); + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Func action = () => + { + Func> func = () => taskFactory.Task; + + return func.Should(timer).CompleteWithinAsync(100.Milliseconds()); + }; + + timer.RunsIntoTimeout(); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + action.Should().Throw(); + } + } +} diff --git a/Tests/Shared.Specs/TestingTimer.cs b/Tests/Shared.Specs/TestingTimer.cs new file mode 100644 index 0000000000..16538c3f79 --- /dev/null +++ b/Tests/Shared.Specs/TestingTimer.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions.Common; + +namespace FluentAssertions.Specs +{ + /// + /// Implementation of for testing purposes only. + /// + internal class TestingTimer : ITimer + { + private TaskCompletionSource Signal { get; } = new TaskCompletionSource(); + + Task ITimer.DelayAsync(TimeSpan delay, CancellationToken cancellationToken) + { + return Signal.Task; + } + + bool ITimer.Wait(Task task, TimeSpan timeout) + { + Signal.Task.GetAwaiter().GetResult(); + return Signal.Task.Result; + } + + public void CompletesBeforeTimeout() + { + this.Signal.SetResult(true); + } + + public void RunsIntoTimeout() + { + this.Signal.SetResult(false); + } + } +}