diff --git a/Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs b/Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs new file mode 100644 index 00000000..e1b12ae9 --- /dev/null +++ b/Source/Mockolate/Exceptions/MockVerificationTimeoutException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Mockolate.Exceptions; + +/// +/// Represents a verification timeout error on the mock. +/// +internal class MockVerificationTimeoutException : MockException +{ + /// + public MockVerificationTimeoutException(TimeSpan? timeout, Exception innerException) + : base(timeout is null ? "it timed out" : $"it timed out after {timeout.Value}", innerException) + { + Timeout = timeout; + } + + /// + /// The timeout that was reached during verification, if any. + /// + public TimeSpan? Timeout { get; } +} diff --git a/Source/Mockolate/Verify/IAsyncVerificationResult.cs b/Source/Mockolate/Verify/IAsyncVerificationResult.cs new file mode 100644 index 00000000..9412eafc --- /dev/null +++ b/Source/Mockolate/Verify/IAsyncVerificationResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using Mockolate.Interactions; + +namespace Mockolate.Verify; + +/// +/// An awaitable that uses the timeout or cancellation token to wait for the +/// expected interactions to occur. +/// +public interface IAsyncVerificationResult : IVerificationResult +{ + /// + /// Asynchronously waits until the specified holds true for the current set of + /// interactions, or until the timeout or cancellation token is triggered. + /// + Task VerifyAsync(Func predicate); +} diff --git a/Source/Mockolate/Verify/VerificationResult.cs b/Source/Mockolate/Verify/VerificationResult.cs index 676d2ec8..e474918e 100644 --- a/Source/Mockolate/Verify/VerificationResult.cs +++ b/Source/Mockolate/Verify/VerificationResult.cs @@ -1,5 +1,8 @@ using System; using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Mockolate.Exceptions; using Mockolate.Interactions; namespace Mockolate.Verify; @@ -37,6 +40,153 @@ TVerify IVerificationResult.Object internal VerificationResult Map(T mock) => new(mock, _interactions, _predicate, _expectation); + /// + /// Makes the verification result awaitable, using the specified to wait for the expected + /// interactions to occur. + /// + public virtual VerificationResult Within(TimeSpan timeout) + => new Awaitable(this, timeout); + + /// + /// Makes the verification result awaitable, using the specified to wait for the + /// expected interactions to occur. + /// + public virtual VerificationResult WithCancellation(CancellationToken cancellationToken) + => new Awaitable(this, cancellationToken); + + /// + /// An awaitable that uses the timeout or cancellation token to wait for the + /// expected interactions to occur. + /// + internal class Awaitable : VerificationResult, IAsyncVerificationResult + { + private CancellationToken? _cancellationToken; + private TimeSpan? _timeout; + + /// + /// An awaitable that uses the to wait for the + /// expected interactions to occur. + /// + public Awaitable(VerificationResult inner, TimeSpan timeout) : base(inner._verify, inner._interactions, + inner._predicate, inner._expectation) + { + _timeout = timeout; + } + + /// + /// An awaitable that uses the to wait + /// for the + /// expected interactions to occur. + /// + public Awaitable(VerificationResult inner, CancellationToken cancellationToken) : base(inner._verify, + inner._interactions, inner._predicate, inner._expectation) + { + _cancellationToken = cancellationToken; + } + + /// + bool IVerificationResult.Verify(Func predicate) + { + IInteraction[] matchingInteractions = _interactions.Interactions.Where(_predicate).ToArray(); + _interactions.Verified(matchingInteractions); + bool result = predicate(matchingInteractions); + if (result) + { + return true; + } + + return VerifyAsync(predicate).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + public async Task VerifyAsync(Func predicate) + { + IInteraction[] matchingInteractions = _interactions.Interactions.Where(_predicate).ToArray(); + _interactions.Verified(matchingInteractions); + bool result = predicate(matchingInteractions); + if (result) + { + return true; + } + + try + { + CancellationTokenSource? cts = null; + CancellationToken token; + if (_timeout is null) + { + token = _cancellationToken!.Value; + } + else + { + if (_cancellationToken is not null) + { + cts = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken.Value); + } + else + { + cts = new CancellationTokenSource(); + } + + cts.CancelAfter(_timeout.Value); + token = cts.Token; + } + + SemaphoreSlim semaphore = new(0, 1); + try + { + _interactions.InteractionAdded += OnInteractionAdded; + do + { + await semaphore.WaitAsync(token); + + matchingInteractions = _interactions.Interactions.Where(_predicate).ToArray(); + _interactions.Verified(matchingInteractions); + if (predicate(matchingInteractions)) + { + return true; + } + } while (!token.IsCancellationRequested); + + return false; + } + finally + { + _interactions.InteractionAdded -= OnInteractionAdded; + cts?.Dispose(); + } + + void OnInteractionAdded(object? sender, EventArgs eventArgs) + { + semaphore.Release(); + } + } + catch (OperationCanceledException ex) + { + if (_cancellationToken?.IsCancellationRequested == true) + { + throw new MockVerificationTimeoutException(null, ex); + } + + throw new MockVerificationTimeoutException(_timeout, ex); + } + } + + /// + public override VerificationResult Within(TimeSpan timeout) + { + _timeout = timeout; + return this; + } + + /// + public override VerificationResult WithCancellation(CancellationToken cancellationToken) + { + _cancellationToken = cancellationToken; + return this; + } + } + #region IVerificationResult /// diff --git a/Source/Mockolate/Verify/VerificationResultExtensions.cs b/Source/Mockolate/Verify/VerificationResultExtensions.cs index 8ff86156..969959dc 100644 --- a/Source/Mockolate/Verify/VerificationResultExtensions.cs +++ b/Source/Mockolate/Verify/VerificationResultExtensions.cs @@ -33,15 +33,23 @@ private static string ToTimes(this int amount, string verb = "") public void AtLeast(int times) { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length >= times; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length >= times; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} at least {times.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} at least {times.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} at least {times.ToTimes()}, but {timeoutException.Message}."); } } @@ -51,15 +59,23 @@ public void AtLeast(int times) public void AtLeastOnce() { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length >= 1; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length >= 1; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} at least {1.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} at least {1.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} at least {1.ToTimes()}, but {timeoutException.Message}."); } } @@ -69,15 +85,23 @@ public void AtLeastOnce() public void AtLeastTwice() { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length >= 2; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length >= 2; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} at least {2.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} at least {2.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} at least {2.ToTimes()}, but {timeoutException.Message}."); } } @@ -87,15 +111,23 @@ public void AtLeastTwice() public void AtMost(int times) { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length <= times; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length <= times; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} at most {times.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} at most {times.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} at most {times.ToTimes()}, but {timeoutException.Message}."); } } @@ -117,15 +149,23 @@ public void Between(int minimum, int maximum) } IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length >= minimum && interactions.Length <= maximum; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length >= minimum && interactions.Length <= maximum; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} between {minimum} and {maximum} times, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} between {minimum} and {maximum} times, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} between {minimum} and {maximum} times, but {timeoutException.Message}."); } } @@ -135,15 +175,23 @@ public void Between(int minimum, int maximum) public void AtMostOnce() { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length <= 1; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length <= 1; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} at most {1.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} at most {1.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} at most {1.ToTimes()}, but {timeoutException.Message}."); } } @@ -153,15 +201,23 @@ public void AtMostOnce() public void AtMostTwice() { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length <= 2; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length <= 2; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} at most {2.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} at most {2.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} at most {2.ToTimes()}, but {timeoutException.Message}."); } } @@ -171,15 +227,23 @@ public void AtMostTwice() public void Exactly(int times) { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length == times; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length == times; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} exactly {times.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} exactly {times.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} exactly {times.ToTimes()}, but {timeoutException.Message}."); } } @@ -189,15 +253,23 @@ public void Exactly(int times) public void Never() { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length == 0; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length == 0; + })) + { + throw new MockVerificationException( + $"Expected that mock {0.ToTimes()} {result.Expectation}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {0.ToTimes()} {result.Expectation}, but it {found.ToTimes("did")}."); + $"Expected that mock {0.ToTimes()} {result.Expectation}, but {timeoutException.Message}."); } } @@ -207,15 +279,23 @@ public void Never() public void Once() { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length == 1; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length == 1; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} exactly {1.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} exactly {1.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} exactly {1.ToTimes()}, but {timeoutException.Message}."); } } @@ -225,15 +305,23 @@ public void Once() public void Twice() { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return interactions.Length == 2; - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return interactions.Length == 2; + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} exactly {2.ToTimes()}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} exactly {2.ToTimes()}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} exactly {2.ToTimes()}, but {timeoutException.Message}."); } } @@ -245,15 +333,23 @@ public void Times(Func predicate, string doNotPopulateThisValue = "") { IVerificationResult result = verificationResult; - int found = 0; - if (!result.Verify(interactions => - { - found = interactions.Length; - return predicate(interactions.Length); - })) + try + { + int found = 0; + if (!result.Verify(interactions => + { + found = interactions.Length; + return predicate(interactions.Length); + })) + { + throw new MockVerificationException( + $"Expected that mock {result.Expectation} according to the predicate {doNotPopulateThisValue}, but it {found.ToTimes("did")}."); + } + } + catch (MockVerificationTimeoutException timeoutException) { throw new MockVerificationException( - $"Expected that mock {result.Expectation} according to the predicate {doNotPopulateThisValue}, but it {found.ToTimes("did")}."); + $"Expected that mock {result.Expectation} according to the predicate {doNotPopulateThisValue}, but {timeoutException.Message}."); } } diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index 5b8cc9fd..61f2f114 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -1729,6 +1729,10 @@ namespace Mockolate.Setup } namespace Mockolate.Verify { + public interface IAsyncVerificationResult : Mockolate.Verify.IVerificationResult + { + System.Threading.Tasks.Task VerifyAsync(System.Func predicate); + } public interface IMockVerifyGotIndexerProtected : Mockolate.IInteractiveMock { } public interface IMockVerifyGotIndexer : Mockolate.IInteractiveMock { } public interface IMockVerifyGotProtected : Mockolate.IInteractiveMock { } @@ -1797,6 +1801,8 @@ namespace Mockolate.Verify public class VerificationResult : Mockolate.Verify.IVerificationResult, Mockolate.Verify.IVerificationResult { public VerificationResult(TVerify verify, Mockolate.Interactions.MockInteractions interactions, System.Func predicate, string expectation) { } + public virtual Mockolate.Verify.VerificationResult WithCancellation(System.Threading.CancellationToken cancellationToken) { } + public virtual Mockolate.Verify.VerificationResult Within(System.TimeSpan timeout) { } } } namespace Mockolate.Web diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index a8f4c048..481ab96a 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -1728,6 +1728,10 @@ namespace Mockolate.Setup } namespace Mockolate.Verify { + public interface IAsyncVerificationResult : Mockolate.Verify.IVerificationResult + { + System.Threading.Tasks.Task VerifyAsync(System.Func predicate); + } public interface IMockVerifyGotIndexerProtected : Mockolate.IInteractiveMock { } public interface IMockVerifyGotIndexer : Mockolate.IInteractiveMock { } public interface IMockVerifyGotProtected : Mockolate.IInteractiveMock { } @@ -1796,6 +1800,8 @@ namespace Mockolate.Verify public class VerificationResult : Mockolate.Verify.IVerificationResult, Mockolate.Verify.IVerificationResult { public VerificationResult(TVerify verify, Mockolate.Interactions.MockInteractions interactions, System.Func predicate, string expectation) { } + public virtual Mockolate.Verify.VerificationResult WithCancellation(System.Threading.CancellationToken cancellationToken) { } + public virtual Mockolate.Verify.VerificationResult Within(System.TimeSpan timeout) { } } } namespace Mockolate.Web diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index 6e79b660..5acc6777 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -1677,6 +1677,10 @@ namespace Mockolate.Setup } namespace Mockolate.Verify { + public interface IAsyncVerificationResult : Mockolate.Verify.IVerificationResult + { + System.Threading.Tasks.Task VerifyAsync(System.Func predicate); + } public interface IMockVerifyGotIndexerProtected : Mockolate.IInteractiveMock { } public interface IMockVerifyGotIndexer : Mockolate.IInteractiveMock { } public interface IMockVerifyGotProtected : Mockolate.IInteractiveMock { } @@ -1745,6 +1749,8 @@ namespace Mockolate.Verify public class VerificationResult : Mockolate.Verify.IVerificationResult, Mockolate.Verify.IVerificationResult { public VerificationResult(TVerify verify, Mockolate.Interactions.MockInteractions interactions, System.Func predicate, string expectation) { } + public virtual Mockolate.Verify.VerificationResult WithCancellation(System.Threading.CancellationToken cancellationToken) { } + public virtual Mockolate.Verify.VerificationResult Within(System.TimeSpan timeout) { } } } namespace Mockolate.Web diff --git a/Tests/Mockolate.Internal.Tests/MockVerificationTimeoutExceptionTests.cs b/Tests/Mockolate.Internal.Tests/MockVerificationTimeoutExceptionTests.cs new file mode 100644 index 00000000..2b04f9df --- /dev/null +++ b/Tests/Mockolate.Internal.Tests/MockVerificationTimeoutExceptionTests.cs @@ -0,0 +1,28 @@ +using Mockolate.Exceptions; + +namespace Mockolate.Internal.Tests; + +public class MockVerificationTimeoutExceptionTests +{ + [Fact] + public async Task WithoutTimeout_ShouldHaveTimedOutMessage() + { + Exception exception = new("foo"); + MockVerificationTimeoutException sut = new(null, exception); + + await That(sut.Timeout).IsNull(); + await That(sut.Message).IsEqualTo("it timed out"); + await That(sut.InnerException).IsSameAs(exception); + } + + [Fact] + public async Task WithTimeout_ShouldIncludeTimeoutInMessage() + { + Exception exception = new("foo"); + MockVerificationTimeoutException sut = new(TimeSpan.FromSeconds(30), exception); + + await That(sut.Timeout).IsEqualTo(TimeSpan.FromSeconds(30)); + await That(sut.Message).IsEqualTo("it timed out after 00:00:30"); + await That(sut.InnerException).IsSameAs(exception); + } +} diff --git a/Tests/Mockolate.Tests/Verify/VerificationResultExtensionsTests.cs b/Tests/Mockolate.Tests/Verify/VerificationResultExtensionsTests.cs index 21dfbcfd..af015711 100644 --- a/Tests/Mockolate.Tests/Verify/VerificationResultExtensionsTests.cs +++ b/Tests/Mockolate.Tests/Verify/VerificationResultExtensionsTests.cs @@ -28,6 +28,22 @@ await That(Act).Throws().OnlyIf(!expectSuccess) "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at least 3 times, but it did twice."); } + [Fact] + public async Task AtLeast_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).AtLeast(5); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at least 5 times, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, false)] [InlineData(1, true)] @@ -48,6 +64,22 @@ await That(Act).Throws().OnlyIf(!expectSuccess) "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at least once, but it never did."); } + [Fact] + public async Task AtLeastOnce_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at least once, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, false)] [InlineData(1, false)] @@ -68,6 +100,22 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at least twice, but it {count switch { 0 => "never did", _ => "did once", }}."); } + [Fact] + public async Task AtLeastTwice_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).AtLeastTwice(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at least twice, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, 0, true)] [InlineData(2, 1, false)] @@ -88,6 +136,23 @@ await That(Act).Throws().OnlyIf(!expectSuccess) "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at most once, but it did twice."); } + [Fact] + public async Task AtMost_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + ExecuteDoSomethingOn(mock, 5); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).AtMost(4); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at most 4 times, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, true)] [InlineData(1, true)] @@ -108,6 +173,23 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at most once, but it did {(count == 2 ? "twice" : $"{count} times")}."); } + [Fact] + public async Task AtMostOnce_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + ExecuteDoSomethingOn(mock, 2); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).AtMostOnce(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at most once, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, true)] [InlineData(1, true)] @@ -128,6 +210,23 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at most twice, but it did {count} times."); } + [Fact] + public async Task AtMostTwice_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + ExecuteDoSomethingOn(mock, 3); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).AtMostTwice(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) at most twice, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, 0, 0, true)] [InlineData(1, 0, 2, true)] @@ -159,6 +258,22 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) between {minimum} and {maximum} times, but it {expectedDidTimes}."); } + [Fact] + public async Task Between_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).Between(3, 5); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) between 3 and 5 times, but it timed out after 00:00:00.0200000."); + } + [Fact] public async Task Between_WithMaximumLessThanMinimum_ShouldThrowArgumentOutOfRangeException() { @@ -209,6 +324,22 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) exactly {(times == 1 ? "once" : $"{times} times")}, but it did twice."); } + [Fact] + public async Task Exactly_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).Exactly(3); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) exactly 3 times, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, true)] [InlineData(1, false)] @@ -229,6 +360,23 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock never invoked method Dispense(It.IsAny(), It.IsAny()), but it did {count switch { 1 => "once", 2 => "twice", _ => $"{count} times", }}."); } + [Fact] + public async Task Never_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + ExecuteDoSomethingOn(mock, 1); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).Never(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock never invoked method Dispense(It.IsAny(), It.IsAny()), but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, false)] [InlineData(1, true)] @@ -249,6 +397,22 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) exactly once, but it {count switch { 0 => "never did", 2 => "did twice", _ => $"did {count} times", }}."); } + [Fact] + public async Task Once_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).Once(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) exactly once, but it timed out after 00:00:00.0200000."); + } + [Fact] public async Task Then_ShouldVerifyInOrder() { @@ -296,23 +460,6 @@ void Act() await That(Act).Throws().OnlyIf(!expectMatch); } - [Fact] - public async Task Then_WhenOnlyPartlyMatch_ShouldFail() - { - IChocolateDispenser mock = Mock.Create(); - mock.Dispense("Dark", 1); - mock.Dispense("Dark", 2); - mock.Dispense("Dark", 3); - mock.Dispense("Dark", 4); - - await That(void () => mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.Is(2)) - .Then(m => m.Invoked.Dispense(It.IsAny(), It.Is(1)), - m => m.Invoked.Dispense(It.IsAny(), It.Is(4)))) - .Throws() - .WithMessage( - "Expected that mock invoked method Dispense(It.IsAny(), 2), then invoked method Dispense(It.IsAny(), 1), then invoked method Dispense(It.IsAny(), 4) in order, but it invoked method Dispense(It.IsAny(), 1) too early."); - } - [Fact] public async Task Then_WhenNoMatch_ShouldFail() { @@ -358,6 +505,39 @@ await That(Act).Throws() .WithMessage("The subject is no mock subject."); } + [Fact] + public async Task Then_WhenOnlyPartlyMatch_ShouldFail() + { + IChocolateDispenser mock = Mock.Create(); + mock.Dispense("Dark", 1); + mock.Dispense("Dark", 2); + mock.Dispense("Dark", 3); + mock.Dispense("Dark", 4); + + await That(void () => mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.Is(2)) + .Then(m => m.Invoked.Dispense(It.IsAny(), It.Is(1)), + m => m.Invoked.Dispense(It.IsAny(), It.Is(4)))) + .Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), 2), then invoked method Dispense(It.IsAny(), 1), then invoked method Dispense(It.IsAny(), 4) in order, but it invoked method Dispense(It.IsAny(), 1) too early."); + } + + [Fact] + public async Task Times_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).Times(_ => false); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) according to the predicate _ => false, but it timed out after 00:00:00.0200000."); + } + [Theory] [InlineData(0, true)] [InlineData(1, false)] @@ -469,6 +649,22 @@ await That(Act).Throws().OnlyIf(!expectSuccess) $"Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) exactly twice, but it {count switch { 0 => "never did", 1 => "did once", _ => $"did {count} times", }}."); } + [Fact] + public async Task Twice_WhenTimedOut_ShouldThrowMockVerificationException() + { + IChocolateDispenser mock = Mock.Create(); + + void Act() + { + mock.VerifyMock.Invoked.Dispense(It.IsAny(), It.IsAny()) + .Within(TimeSpan.FromMilliseconds(20)).Twice(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(It.IsAny(), It.IsAny()) exactly twice, but it timed out after 00:00:00.0200000."); + } + private class MyChocolateDispenser : IChocolateDispenser { private readonly Dictionary _inventory = new(); diff --git a/Tests/Mockolate.Tests/Verify/VerificationResultTests.AsyncTests.cs b/Tests/Mockolate.Tests/Verify/VerificationResultTests.AsyncTests.cs new file mode 100644 index 00000000..3f7a2f2d --- /dev/null +++ b/Tests/Mockolate.Tests/Verify/VerificationResultTests.AsyncTests.cs @@ -0,0 +1,144 @@ +using System.Diagnostics; +using System.Threading; +using Mockolate.Exceptions; +using Mockolate.Tests.TestHelpers; +using Mockolate.Verify; + +namespace Mockolate.Tests.Verify; + +public sealed partial class VerificationResultTests +{ + public class AsyncTests + { + [Fact] + public async Task MultipleWithin_ShouldOverwritePreviousTimeout() + { + IChocolateDispenser sut = Mock.Create(); + + void Act() + { + sut.VerifyMock.Invoked.Dispense(Match.AnyParameters()) + .Within(TimeSpan.FromMilliseconds(100)) + .Within(TimeSpan.FromMilliseconds(200)) + .AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(Match.AnyParameters()) at least once, but it timed out after 00:00:00.2000000."); + } + + [Fact] + public async Task WhenAlreadySuccessful_ShouldSucceed() + { + IChocolateDispenser sut = Mock.Create(); + sut.Dispense("Dark", 1); + sut.Dispense("Dark", 2); + + await That(sut.VerifyMock.Invoked.Dispense(Match.AnyParameters()).Within(TimeSpan.FromMilliseconds(500))) + .AtLeastOnce(); + } + + [Fact] + public async Task WithCancellationAndTimeout_ShouldCombineBoth() + { + IChocolateDispenser sut = Mock.Create(); + using CancellationTokenSource cts = new(50); + CancellationToken token = cts.Token; + + void Act() + { + sut.VerifyMock.Invoked.Dispense(Match.AnyParameters()) + .Within(TimeSpan.FromMilliseconds(30000)) + .WithCancellation(token) + .AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(Match.AnyParameters()) at least once, but it timed out."); + } + + [Fact] + public async Task WithCancellationAndTimeout_ShouldIncludeTimeoutInExceptionWhenLessThanCancellationToken() + { + IChocolateDispenser sut = Mock.Create(); + using CancellationTokenSource cts = new(30000); + CancellationToken token = cts.Token; + + void Act() + { + sut.VerifyMock.Invoked.Dispense(Match.AnyParameters()) + .Within(TimeSpan.FromMilliseconds(50)) + .WithCancellation(token) + .AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(Match.AnyParameters()) at least once, but it timed out after 00:00:00.0500000."); + } + + [Fact] + public async Task WithCancellationToken_ShouldIncludeTimeoutInException() + { + IChocolateDispenser sut = Mock.Create(); + using CancellationTokenSource cts = new(100); + CancellationToken token = cts.Token; + + void Act() + { + sut.VerifyMock.Invoked.Dispense(Match.AnyParameters()).WithCancellation(token).AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(Match.AnyParameters()) at least once, but it timed out."); + } + + [Fact] + public async Task Within_ShouldAbortAsSoonAsConditionIsSatisfied() + { + IChocolateDispenser sut = Mock.Create(); + using CancellationTokenSource cts = new(); + CancellationToken token = cts.Token; + + _ = Task.Run(async () => + { + for (int i = 0; i < 100; i++) + { + await Task.Delay(100); + sut.Dispense("Dark", i); + if (token.IsCancellationRequested) + { + break; + } + } + }, token); + + Stopwatch sw = Stopwatch.StartNew(); + sut.VerifyMock.Invoked.Dispense(Match.AnyParameters()).Within(TimeSpan.FromMilliseconds(500)) + .AtLeastOnce(); + sw.Stop(); + cts.Cancel(); + + await That(sw.Elapsed).IsLessThan(TimeSpan.FromSeconds(1)); + } + + [Fact] + public async Task WithTimeout_ShouldIncludeTimeoutInException() + { + IChocolateDispenser sut = Mock.Create(); + + void Act() + { + sut.VerifyMock.Invoked.Dispense(Match.AnyParameters()).Within(TimeSpan.FromMilliseconds(100)) + .AtLeastOnce(); + } + + await That(Act).Throws() + .WithMessage( + "Expected that mock invoked method Dispense(Match.AnyParameters()) at least once, but it timed out after 00:00:00.1000000."); + } + } +} diff --git a/Tests/Mockolate.Tests/Verify/VerificationResultTests.cs b/Tests/Mockolate.Tests/Verify/VerificationResultTests.cs index c88f9895..be94a3f0 100644 --- a/Tests/Mockolate.Tests/Verify/VerificationResultTests.cs +++ b/Tests/Mockolate.Tests/Verify/VerificationResultTests.cs @@ -3,7 +3,7 @@ namespace Mockolate.Tests.Verify; -public class VerificationResultTests +public sealed partial class VerificationResultTests { [Fact] public async Task VerificationResult_Got_ShouldHaveExpectedValue()