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