Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setup.Verifiable(Times times, [string failMessage]) method #1319

Merged
merged 3 commits into from
Dec 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1
#### Added

* `Mock<T>.RaiseAsync` method for raising "async" events, i.e. events that use a `Func<..., Task>` or `Func<..., ValueTask>` delegate. (@stakx, #1313)

* `setup.Verifiable(Times times, [string failMessage])` method to specify the expected number of calls upfront. `mock.Verify[All]` can then be used to check whether the setup was called that many times. The upper bound (maximum allowed number of calls) will be checked right away, i.e. whenever a setup gets called. (@stakx, #1319)

## 4.18.4 (2022-12-30)

Expand Down
41 changes: 0 additions & 41 deletions src/Moq/Behaviors/LimitInvocationCount.cs

This file was deleted.

47 changes: 47 additions & 0 deletions src/Moq/Behaviors/VerifyInvocationCount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

namespace Moq.Behaviors
{
internal sealed class VerifyInvocationCount : Behavior
{
private int count;
private readonly Times times;
private readonly MethodCall setup;

public VerifyInvocationCount(MethodCall setup, Times times)
{
this.setup = setup;
this.times = times;
this.count = 0;
}

public void Reset()
{
this.count = 0;
}

public override void Execute(Invocation invocation)
{
++this.count;
this.VerifyUpperBound();
}

public void Verify()
{
if (!this.times.Validate(this.count))
{
throw MockException.IncorrectNumberOfCalls(this.setup, this.times, this.count);
}
}

public void VerifyUpperBound()
{
var (_, maxCount) = this.times;
if (this.count > maxCount)
{
throw MockException.IncorrectNumberOfCalls(this.setup, this.times, this.count);
}
}
}
}
24 changes: 22 additions & 2 deletions src/Moq/Language/Flow/SetupPhrase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ protected SetupPhrase(MethodCall setup)

public IVerifies AtMost(int callCount)
{
this.setup.AtMost(callCount);
this.setup.SetExpectedInvocationCount(Times.AtMost(callCount));
return this;
}

public IVerifies AtMostOnce() => this.AtMost(1);
public IVerifies AtMostOnce()
{
this.setup.SetExpectedInvocationCount(Times.AtMostOnce());
return this;
}

public ICallbackResult Callback(InvocationAction action)
{
Expand Down Expand Up @@ -278,6 +282,22 @@ public void Verifiable(string failMessage)
this.setup.SetFailMessage(failMessage);
}

public void Verifiable(Func<Times> times) => this.Verifiable(times(), null);

public void Verifiable(Times times) => this.Verifiable(times, null);

public void Verifiable(Func<Times> times, string failMessage) => this.Verifiable(times(), failMessage);

public void Verifiable(Times times, string failMessage)
{
this.setup.MarkAsVerifiable();
this.setup.SetExpectedInvocationCount(times);
if (failMessage != null)
{
this.setup.SetFailMessage(failMessage);
}
}

public override string ToString()
{
return setup.Expression.ToStringFixed();
Expand Down
31 changes: 31 additions & 0 deletions src/Moq/Language/IVerifies.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;
using System.ComponentModel;

namespace Moq.Language
Expand Down Expand Up @@ -40,5 +41,35 @@ public interface IVerifies : IFluentInterface
/// </code>
/// </example>
void Verifiable(string failMessage);

/// <summary>
/// Marks the setup as verifiable and specifies the number of expected calls.
/// <see cref="Mock.Verify()"/> and <see cref="Mock.VerifyAll()"/> will later verify
/// that the setup was called the correct number of times.
/// </summary>
void Verifiable(Times times);

/// <summary>
/// Marks the setup as verifiable and specifies the number of expected calls.
/// <see cref="Mock.Verify()"/> and <see cref="Mock.VerifyAll()"/> will later verify
/// that the setup was called the correct number of times.
/// </summary>
void Verifiable(Func<Times> times);

/// <summary>
/// Marks the setup as verifiable and specifies the number of expected calls
/// and a message for failures.
/// <see cref="Mock.Verify()"/> and <see cref="Mock.VerifyAll()"/> will later verify
/// that the setup was called the correct number of times.
/// </summary>
void Verifiable(Times times, string failMessage);

/// <summary>
/// Marks the setup as verifiable and specifies the number of expected calls
/// and a message for failures.
/// <see cref="Mock.Verify()"/> and <see cref="Mock.VerifyAll()"/> will later verify
/// that the setup was called the correct number of times.
/// </summary>
void Verifiable(Func<Times> times, string failMessage);
}
}
22 changes: 17 additions & 5 deletions src/Moq/MethodCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Moq
{
internal sealed partial class MethodCall : SetupWithOutParameterSupport
{
private LimitInvocationCount limitInvocationCount;
private VerifyInvocationCount verifyInvocationCount;
private Behavior callback;
private Behavior raiseEvent;
private Behavior returnOrThrow;
Expand Down Expand Up @@ -99,7 +99,7 @@ private static string GetUserCodeCallSite()

protected override void ExecuteCore(Invocation invocation)
{
this.limitInvocationCount?.Execute(invocation);
this.verifyInvocationCount?.Execute(invocation);

this.callback?.Execute(invocation);

Expand Down Expand Up @@ -339,12 +339,24 @@ public void SetThrowComputedExceptionBehavior(Delegate exceptionFactory)

protected override void ResetCore()
{
this.limitInvocationCount?.Reset();
this.verifyInvocationCount?.Reset();
}

public void AtMost(int count)
public void SetExpectedInvocationCount(Times times)
{
this.limitInvocationCount = new LimitInvocationCount(this, count);
this.verifyInvocationCount = new VerifyInvocationCount(this, times);
}

protected override void VerifySelf()
{
if (this.verifyInvocationCount != null)
{
this.verifyInvocationCount.Verify();
}
else
{
base.VerifySelf();
}
}

public override string ToString()
Expand Down
21 changes: 4 additions & 17 deletions src/Moq/MockException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,16 @@ namespace Moq
public class MockException : Exception
{
/// <summary>
/// Returns the exception to be thrown when a setup limited by <see cref="IOccurrence.AtMostOnce()"/> is matched more often than once.
/// Returns the exception to be thrown when a setup has been invoked an incorrect (unexpected) number of times.
/// </summary>
internal static MockException MoreThanOneCall(MethodCall setup, int invocationCount)
internal static MockException IncorrectNumberOfCalls(MethodCall setup, Times times, int invocationCount)
{
var message = new StringBuilder();
message.AppendLine(setup.FailMessage ?? "")
.Append(Times.AtMostOnce().GetExceptionMessage(invocationCount))
.Append(times.GetExceptionMessage(invocationCount))
.AppendLine(setup.Expression.ToStringFixed());

return new MockException(MockExceptionReasons.MoreThanOneCall, message.ToString());
}

/// <summary>
/// Returns the exception to be thrown when a setup limited by <see cref="IOccurrence.AtMost(int)"/> is matched more often than the specified maximum number of times.
/// </summary>
internal static MockException MoreThanNCalls(MethodCall setup, int maxInvocationCount, int invocationCount)
{
var message = new StringBuilder();
message.AppendLine(setup.FailMessage ?? "")
.Append(Times.AtMost(maxInvocationCount).GetExceptionMessage(invocationCount))
.AppendLine(setup.Expression.ToStringFixed());

return new MockException(MockExceptionReasons.MoreThanNCalls, message.ToString());
return new MockException(MockExceptionReasons.IncorrectNumberOfCalls, message.ToString());
}

/// <summary>
Expand Down
3 changes: 1 addition & 2 deletions src/Moq/MockExceptionReasons.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ namespace Moq
[Flags]
internal enum MockExceptionReasons
{
MoreThanOneCall = 1,
MoreThanNCalls = 2,
IncorrectNumberOfCalls = 1,
NoMatchingCalls = 4,
NoSetup = 8,
ReturnValueRequired = 16,
Expand Down
4 changes: 2 additions & 2 deletions src/Moq/Obsolete/IOccurrence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public interface IOccurrence : IFluentInterface
/// .AtMostOnce();
/// </code>
/// </example>
[Obsolete("To verify this condition, use the overload to Verify that receives Times.AtMostOnce().")]
[Obsolete("Use 'mock.Verify(call, Times.AtMostOnce)' or 'setup.Verifiable(Times.AtMostOnce)' instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
IVerifies AtMostOnce();
/// <summary>
Expand All @@ -36,7 +36,7 @@ public interface IOccurrence : IFluentInterface
/// .AtMost( 5 );
/// </code>
/// </example>
[Obsolete("To verify this condition, use the overload to Verify that receives Times.AtMost(callCount).")]
[Obsolete("Use 'mock.Verify(call, Times.AtMost(callCount))' or 'setup.Verifiable(Times.AtMost(callCount))' instead.")]
[EditorBrowsable(EditorBrowsableState.Never)]
IVerifies AtMost(int callCount);
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Moq.Tests/InvocationsFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ public void Invocations_Clear_resets_count_kept_by_setup_AtMost()
mock.Invocations.Clear();
_ = mock.Object.CompareTo(default); // this second call should now count as the first
var ex = Assert.Throws<MockException>(() => mock.Object.CompareTo(default));
Assert.Equal(MockExceptionReasons.MoreThanOneCall, ex.Reasons);
Assert.Equal(MockExceptionReasons.IncorrectNumberOfCalls, ex.Reasons);
}

[Fact]
Expand Down
4 changes: 2 additions & 2 deletions tests/Moq.Tests/OccurrenceFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public void OnceThrowsOnSecondCall()

Assert.Equal("ack", mock.Object.Execute("ping"));
MockException mex = Assert.Throws<MockException>(() => mock.Object.Execute("ping"));
Assert.Equal(MockExceptionReasons.MoreThanOneCall, mex.Reasons);
Assert.Equal(MockExceptionReasons.IncorrectNumberOfCalls, mex.Reasons);
}

[Fact]
Expand All @@ -45,7 +45,7 @@ public void RepeatThrowsOnNPlusOneCall()
Assert.True(false, "should fail on two calls");
});

Assert.Equal(MockExceptionReasons.MoreThanNCalls, mex.Reasons);
Assert.Equal(MockExceptionReasons.IncorrectNumberOfCalls, mex.Reasons);
Assert.Equal(calls, repeat);
}

Expand Down
Loading