Skip to content

Commit

Permalink
#825 event handlers setup and verify (#857)
Browse files Browse the repository at this point in the history
* WIP: initial feature commit
* Thread-safe hasEventSetup check
* Fix Resources
* PR review comments processed
* CHANGELOG.md updated
* Fix codefactor violation.
  • Loading branch information
lepijohnny authored and stakx committed Jul 24, 2019
1 parent c59a3b3 commit bf7ee69
Show file tree
Hide file tree
Showing 13 changed files with 680 additions and 13 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1

* Improved error message that is supplied with `ArgumentException` thrown when `Setup` or `Verify` are called on a protected method if the method could not be found with both the name and compatible argument types specified (@thomasfdm, #852).

#### Added

* Added support for setup and verification of the event handlers through `Setup[Add|Remove]` and `Verify[Add|Remove|All]` (@lepijohnny, #825)

## 4.12.0 (2019-06-20)

#### Changed
Expand Down
12 changes: 12 additions & 0 deletions src/Moq/ExpressionStringBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,18 @@ private void ToStringMethodCall(MethodCallExpression node)
this.builder.Append('.').Append(node.Method.Name.Substring(4)).Append(" = ");
ToString(node.Arguments.Last());
}
else if (node.Method.LooksLikeEventAttach())
{
this.builder.Append('.')
.AppendNameOfAddEvent(node.Method, includeGenericArgumentList: true);
AsCommaSeparatedValues(node.Arguments.Skip(paramFrom), ToString);
}
else if (node.Method.LooksLikeEventDetach())
{
this.builder.Append('.')
.AppendNameOfRemoveEvent(node.Method, includeGenericArgumentList: true);
AsCommaSeparatedValues(node.Arguments.Skip(paramFrom), ToString);
}
else
{
this.builder
Expand Down
40 changes: 40 additions & 0 deletions src/Moq/Guard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,46 @@ public static void IsVisibleToProxyFactory(MethodInfo method)
}
}

public static void IsEventAttach(LambdaExpression expression, string paramName)
{
Debug.Assert(expression != null);

switch (expression.Body.NodeType)
{
case ExpressionType.Call:
var call = (MethodCallExpression)expression.Body;
if (call.Method.LooksLikeEventAttach()) return;
break;
}

throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture,
Resources.SetupNotEventAttach,
expression.ToStringFixed()),
paramName);
}

public static void IsEventDetach(LambdaExpression expression, string paramName)
{
Debug.Assert(expression != null);

switch (expression.Body.NodeType)
{
case ExpressionType.Call:
var call = (MethodCallExpression)expression.Body;
if (call.Method.LooksLikeEventDetach()) return;
break;
}

throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture,
Resources.SetupNotEventDetach,
expression.ToStringFixed()),
paramName);
}

/// <summary>
/// Ensures the given <paramref name="value"/> is not null.
/// Throws <see cref="ArgumentNullException"/> otherwise.
Expand Down
34 changes: 26 additions & 8 deletions src/Moq/Interception/InterceptionAspects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,26 @@ public static bool Handle(Invocation invocation, Mock mock)
// If they are equal, then `invocation.Method` is definitely an event `add` accessor.
// Not sure whether this would work with F# and COM; see commit 44070a9.

bool doesntHaveEventSetup = !mock.Setups.HasEventSetup;

if (mock.CallBase && !invocation.Method.IsAbstract)
{
invocation.ReturnBase();
return true;
if(doesntHaveEventSetup)
{
invocation.ReturnBase();
}
}
else if (invocation.Arguments.Length > 0 && invocation.Arguments[0] is Delegate delegateInstance)
{
mock.EventHandlers.Add(eventInfo.Name, delegateInstance);
invocation.Return();
return true;

if (doesntHaveEventSetup)
{
invocation.Return();
}
}

return doesntHaveEventSetup;
}
}
else if (methodName[0] == 'r' && methodName.Length > 7 && methodName[6] == '_' && invocation.Method.LooksLikeEventDetach())
Expand All @@ -170,17 +179,26 @@ public static bool Handle(Invocation invocation, Mock mock)
// If they are equal, then `invocation.Method` is definitely an event `remove` accessor.
// Not sure whether this would work with F# and COM; see commit 44070a9.

bool doesntHaveEventSetup = !mock.Setups.HasEventSetup;

if (mock.CallBase && !invocation.Method.IsAbstract)
{
invocation.ReturnBase();
return true;
if (doesntHaveEventSetup)
{
invocation.ReturnBase();
}
}
else if (invocation.Arguments.Length > 0 && invocation.Arguments[0] is Delegate delegateInstance)
{
mock.EventHandlers.Remove(eventInfo.Name, delegateInstance);
invocation.Return();
return true;

if (doesntHaveEventSetup)
{
invocation.Return();
}
}

return doesntHaveEventSetup;
}
}
}
Expand Down
228 changes: 228 additions & 0 deletions src/Moq/Mock.Generic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,44 @@ public ISetup<T> SetupSet(Action<T> setterExpression)
return new VoidSetupPhrase<T>(setup);
}

/// <summary>
/// Specifies a setup on the mocked type for a call to an event add.
/// </summary>
/// <param name="addExpression">Lambda expression that adds an event.</param>
/// <remarks>
/// If more than one setup is set for the same event add,
/// the latest one wins and is the one that will be executed.
/// </remarks>
/// <include file='Mock.Generic.xdoc' path='docs/doc[@for="Mock{T}.SetupAdd"]/*'/>
public ISetup<T> SetupAdd(Action<T> addExpression)
{
Guard.NotNull(addExpression, nameof(addExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(addExpression, this.ConstructorArguments);

var setup = Mock.SetupAdd(this, expression, condition: null);
return new VoidSetupPhrase<T>(setup);
}

/// <summary>
/// Specifies a setup on the mocked type for a call to an event 'remove.
/// </summary>
/// <param name="removeExpression">Lambda expression that removes an event.</param>
/// <remarks>
/// If more than one setup is set for the same event remove,
/// the latest one wins and is the one that will be executed.
/// </remarks>
/// <include file='Mock.Generic.xdoc' path='docs/doc[@for="Mock{T}.SetupRemove"]/*'/>
public ISetup<T> SetupRemove(Action<T> removeExpression)
{
Guard.NotNull(removeExpression, nameof(removeExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(removeExpression, this.ConstructorArguments);

var setup = Mock.SetupRemove(this, expression, condition: null);
return new VoidSetupPhrase<T>(setup);
}

/// <summary>
/// Specifies that the given property should have "property behavior",
/// meaning that setting its value will cause it to be saved and later returned when the property is requested.
Expand Down Expand Up @@ -859,6 +897,196 @@ public void VerifySet(Action<T> setterExpression, Func<Times> times, string fail
Mock.VerifySet(this, expression , times(), failMessage);
}

/// <summary>
/// Verifies that an event was added to the mock.
/// </summary>
/// <param name="addExpression">Expression to verify.</param>
/// <exception cref="MockException">The invocation was not performed on the mock.</exception>
/// <include file='Mock.Generic.xdoc' path='docs/doc[@for="Mock{T}.VerifyAdd(addExpression)"]/*'/>
public void VerifyAdd(Action<T> addExpression)
{
Guard.NotNull(addExpression, nameof(addExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(addExpression, this.ConstructorArguments);
Mock.VerifyAdd(this, expression, Times.AtLeastOnce(), null);
}

/// <summary>
/// Verifies that an event was added to the mock.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="addExpression">Expression to verify.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyAdd(Action<T> addExpression, Times times)
{
Guard.NotNull(addExpression, nameof(addExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(addExpression, this.ConstructorArguments);
Mock.VerifyAdd(this, expression, times, null);
}

/// <summary>
/// Verifies that an event was added to the mock.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="addExpression">Expression to verify.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyAdd(Action<T> addExpression, Func<Times> times)
{
Guard.NotNull(addExpression, nameof(addExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(addExpression, this.ConstructorArguments);
Mock.VerifyAdd(this, expression, times(), null);
}

/// <summary>
/// Verifies that an event was added to the mock, specifying a failure message.
/// </summary>
/// <param name="addExpression">Expression to verify.</param>
/// <param name="failMessage">Message to show if verification fails.</param>
/// <exception cref="MockException">The invocation was not performed on the mock.</exception>
/// <include file='Mock.Generic.xdoc' path='docs/doc[@for="Mock{T}.VerifyAdd(addExpression,failMessage)"]/*'/>
public void VerifyAdd(Action<T> addExpression, string failMessage)
{
Guard.NotNull(addExpression, nameof(addExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(addExpression, this.ConstructorArguments);
Mock.VerifyAdd(this, expression, Times.AtLeastOnce(), failMessage);
}

/// <summary>
/// Verifies that an event was added to the mock, specifying a failure message.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="addExpression">Expression to verify.</param>
/// <param name="failMessage">Message to show if verification fails.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyAdd(Action<T> addExpression, Times times, string failMessage)
{
Guard.NotNull(addExpression, nameof(addExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(addExpression, this.ConstructorArguments);
Mock.VerifyAdd(this, expression, times, failMessage);
}

/// <summary>
/// Verifies that an event was added to the mock, specifying a failure message.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="addExpression">Expression to verify.</param>
/// <param name="failMessage">Message to show if verification fails.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyAdd(Action<T> addExpression, Func<Times> times, string failMessage)
{
Guard.NotNull(addExpression, nameof(addExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(addExpression, this.ConstructorArguments);
Mock.VerifyAdd(this, expression, times(), failMessage);
}

/// <summary>
/// Verifies that an event was removed from the mock.
/// </summary>
/// <param name="removeExpression">Expression to verify.</param>
/// <exception cref="MockException">The invocation was not performed on the mock.</exception>
/// <include file='Mock.Generic.xdoc' path='docs/doc[@for="Mock{T}.VerifyRemove(removeExpression)"]/*'/>
public void VerifyRemove(Action<T> removeExpression)
{
Guard.NotNull(removeExpression, nameof(removeExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(removeExpression, this.ConstructorArguments);
Mock.VerifyRemove(this, expression, Times.AtLeastOnce(), null);
}

/// <summary>
/// Verifies that an event was removed from the mock.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="removeExpression">Expression to verify.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyRemove(Action<T> removeExpression, Times times)
{
Guard.NotNull(removeExpression, nameof(removeExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(removeExpression, this.ConstructorArguments);
Mock.VerifyRemove(this, expression, times, null);
}

/// <summary>
/// Verifies that an event was removed from the mock.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="removeExpression">Expression to verify.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyRemove(Action<T> removeExpression, Func<Times> times)
{
Guard.NotNull(removeExpression, nameof(removeExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(removeExpression, this.ConstructorArguments);
Mock.VerifyRemove(this, expression, times(), null);
}

/// <summary>
/// Verifies that an event was removed from the mock, specifying a failure message.
/// </summary>
/// <param name="removeExpression">Expression to verify.</param>
/// <param name="failMessage">Message to show if verification fails.</param>
/// <exception cref="MockException">The invocation was not performed on the mock.</exception>
/// <include file='Mock.Generic.xdoc' path='docs/doc[@for="Mock{T}.VerifyRemove(removeExpression,failMessage)"]/*'/>
public void VerifyRemove(Action<T> removeExpression, string failMessage)
{
Guard.NotNull(removeExpression, nameof(removeExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(removeExpression, this.ConstructorArguments);
Mock.VerifyRemove(this, expression, Times.AtLeastOnce(), failMessage);
}

/// <summary>
/// Verifies that an event was removed from the mock, specifying a failure message.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="removeExpression">Expression to verify.</param>
/// <param name="failMessage">Message to show if verification fails.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyRemove(Action<T> removeExpression, Times times, string failMessage)
{
Guard.NotNull(removeExpression, nameof(removeExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(removeExpression, this.ConstructorArguments);
Mock.VerifyRemove(this, expression, times, failMessage);
}

/// <summary>
/// Verifies that an event was removed from the mock, specifying a failure message.
/// </summary>
/// <param name="times">The number of times a method is expected to be called.</param>
/// <param name="removeExpression">Expression to verify.</param>
/// <param name="failMessage">Message to show if verification fails.</param>
/// <exception cref="MockException">
/// The invocation was not called the number of times specified by <paramref name="times"/>.
/// </exception>
public void VerifyRemove(Action<T> removeExpression, Func<Times> times, string failMessage)
{
Guard.NotNull(removeExpression, nameof(removeExpression));

var expression = ExpressionReconstructor.Instance.ReconstructExpression(removeExpression, this.ConstructorArguments);
Mock.VerifyRemove(this, expression, times(), failMessage);
}

/// <summary>
/// Verifies that there were no calls other than those already verified.
/// </summary>
Expand Down
Loading

0 comments on commit bf7ee69

Please sign in to comment.