Skip to content

Commit

Permalink
Merge pull request #468 from stakx/ref-callback-return
Browse files Browse the repository at this point in the history
Add new `Callback` and `Returns` overloads for setting up methods with by-ref parameters
  • Loading branch information
stakx authored Oct 5, 2017
2 parents 1b13f62 + 9b64788 commit 2cd9690
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1
#### Added

* Support for sequential setup of `void` methods (@alexbestul, #463)
* Support for callbacks for methods having `ref` or `out` parameters via two new overloads of `Callback` and `Returns` (@stakx, #468)

#### Fixed

Expand Down
38 changes: 38 additions & 0 deletions Source/Language/ICallback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ namespace Moq.Language
[EditorBrowsable(EditorBrowsableState.Never)]
public partial interface ICallback : IFluentInterface
{
/// <summary>
/// Specifies a callback of any delegate type to invoke when the method is called.
/// This overload specifically allows you to define callbacks for methods with by-ref parameters.
/// By-ref parameters can be assigned to.
/// </summary>
/// <param name="callback">The callback method to invoke. Must have return type <c>void</c> (C#) or be a <c>Sub</c> (VB.NET).</param>
/// <example>
/// Invokes the given callback with the concrete invocation argument value. You can modify
/// by-ref parameters inside the callback.
/// <code>
/// delegate void ExecuteAction(ref Command command);
///
/// Command c = ...;
/// mock.Setup(x => x.Execute(ref c))
/// .Callback(new ExecuteAction((ref Command command) => Console.WriteLine("Executing command...")));
/// </code>
/// </example>
ICallbackResult Callback(Delegate callback);

/// <summary>
/// Specifies a callback to invoke when the method is called.
/// </summary>
Expand Down Expand Up @@ -93,6 +112,25 @@ public partial interface ICallback : IFluentInterface
public partial interface ICallback<TMock, TResult> : IFluentInterface
where TMock : class
{
/// <summary>
/// Specifies a callback of any delegate type to invoke when the method is called.
/// This overload specifically allows you to define callbacks for methods with by-ref parameters.
/// By-ref parameters can be assigned to.
/// </summary>
/// <param name="callback">The callback method to invoke. Must have return type <c>void</c> (C#) or be a <c>Sub</c> (VB.NET).</param>
/// <example>
/// Invokes the given callback with the concrete invocation argument value. You can modify
/// by-ref parameters inside the callback.
/// <code>
/// delegate void ExecuteAction(ref Command command);
///
/// Command c = ...;
/// mock.Setup(x => x.Execute(ref c))
/// .Callback(new ExecuteAction((ref Command command) => Console.WriteLine("Executing command...")));
/// </code>
/// </example>
IReturnsThrows<TMock, TResult> Callback(Delegate callback);

/// <summary>
/// Specifies a callback to invoke when the method is called.
/// </summary>
Expand Down
19 changes: 19 additions & 0 deletions Source/Language/IReturns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ public partial interface IReturns<TMock, TResult> : IFluentInterface
/// </example>
IReturnsResult<TMock> Returns(TResult value);

/// <summary>
/// Specifies a function that will calculate the value to return from the method.
/// This overload specifically allows you to specify a function with by-ref parameters.
/// Those by-ref parameters can be assigned to (though you should probably do that from
/// a <c>Callback</c> instead).
/// </summary>
/// <param name="valueFunction">The function that will calculate the return value.</param>
/// <example group="returns">
/// Return a calculated value when the method is called:
/// <code>
/// delegate bool ExecuteFunc(ref Command command);
///
/// Command c = ...;
/// mock.Setup(x => x.Execute(ref c))
/// .Returns(new ExecuteFunc((ref Command command) => command.IsExecutable));
/// </code>
/// </example>
IReturnsResult<TMock> Returns(Delegate valueFunction);

/// <summary>
/// Specifies a function that will calculate the value to return from the method.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions Source/MethodCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,22 @@ public ICallbackResult Callback(Action callback)
return this;
}

public ICallbackResult Callback(Delegate callback)
{
if (callback == null)
{
throw new ArgumentNullException(nameof(callback));
}

if (callback.GetMethodInfo().ReturnType != typeof(void))
{
throw new ArgumentException(Resources.InvalidCallbackNotADelegateWithReturnTypeVoid, nameof(callback));
}

this.SetCallbackWithArguments(callback);
return this;
}

protected virtual void SetCallbackWithoutArguments(Action callback)
{
this.setupCallback = delegate { callback(); };
Expand Down
34 changes: 34 additions & 0 deletions Source/MethodCallReturn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

using Moq.Language;
using Moq.Language.Flow;
using Moq.Properties;
using Moq.Proxy;
using System;
using System.Linq.Expressions;
Expand Down Expand Up @@ -91,6 +92,33 @@ public IVerifies Raises(Action<TMock> eventExpression, params object[] args)
return this.RaisesImpl(eventExpression, args);
}

public IReturnsResult<TMock> Returns(Delegate valueFunction)
{
// If `TResult` is `Delegate`, that is someone is setting up the return value of a method
// that returns a `Delegate`, then we have arrived here because C# picked the wrong overload:
// We don't want to invoke the passed delegate to get a return value; the passed delegate
// already is the return value.
if (typeof(TResult) == typeof(Delegate))
{
return this.Returns(() => (TResult)(object)valueFunction);
}

// The following may seem overly cautious, but we don't throw an `ArgumentNullException`
// here because Moq has been very forgiving with incorrect `Returns` in the past.
if (valueFunction == null)
{
return this.Returns(() => default(TResult));
}

if (valueFunction.GetMethodInfo().ReturnType == typeof(void))
{
throw new ArgumentException(Resources.InvalidReturnsCallbackNotADelegateWithReturnType, nameof(valueFunction));
}

SetReturnDelegate(valueFunction);
return this;
}

public IReturnsResult<TMock> Returns(Func<TResult> valueExpression)
{
SetReturnDelegate(valueExpression);
Expand All @@ -109,6 +137,12 @@ public IReturnsResult<TMock> CallBase()
return this;
}

IReturnsThrows<TMock, TResult> ICallback<TMock, TResult>.Callback(Delegate callback)
{
base.Callback(callback);
return this;
}

IReturnsThrowsGetter<TMock, TResult> ICallbackGetter<TMock, TResult>.Callback(Action callback)
{
base.Callback(callback);
Expand Down
18 changes: 18 additions & 0 deletions Source/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Source/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,10 @@ Expected invocation on the mock once, but was {4} times: {1}</value>
<value>Cannot set up {0}.{1} because it is not accessible to the proxy generator used by Moq:
{2}</value>
</data>
</root>
<data name="InvalidCallbackNotADelegateWithReturnTypeVoid" xml:space="preserve">
<value>Invalid callback. This overload of the "Callback" method only accepts "void" (C#) or "Sub" (VB.NET) delegates with parameter types matching those of the set up method.</value>
</data>
<data name="InvalidReturnsCallbackNotADelegateWithReturnType" xml:space="preserve">
<value>Invalid callback. This overload of the "Returns" method only accepts non-"void" (C#) or "Function" (VB.NET) delegates with parameter types matching those of the set up method.</value>
</data>
</root>
59 changes: 59 additions & 0 deletions UnitTests/CallbacksFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,59 @@ public void CallbackCanBeImplementedByExtensionMethod()
Assert.Equal("blah extended", receivedArgument);
}

[Fact]
public void CallbackWithRefParameterReceivesArguments()
{
var input = "input";
var received = default(string);

var mock = new Mock<IFoo>();
mock.Setup(f => f.Execute(ref input))
.Callback(new ExecuteRHandler((ref string arg1) =>
{
received = arg1;
}));

mock.Object.Execute(ref input);
Assert.Equal("input", input);
Assert.Equal(input, received);
}

[Fact]
public void CallbackWithRefParameterCanModifyRefParameter()
{
var value = "input";

var mock = new Mock<IFoo>();
mock.Setup(f => f.Execute(ref value))
.Callback(new ExecuteRHandler((ref string arg1) =>
{
arg1 = "output";
}));

Assert.Equal("input", value);
mock.Object.Execute(ref value);
Assert.Equal("output", value);
}

[Fact]
public void CallbackWithRefParameterCannotModifyNonRefParameter()
{
var _ = default(string);
var value = "input";

var mock = new Mock<IFoo>();
mock.Setup(f => f.Execute(ref _, value))
.Callback(new ExecuteRVHandler((ref string arg1, string arg2) =>
{
arg2 = "output";
}));

Assert.Equal("input", value);
mock.Object.Execute(ref _, value);
Assert.Equal("input", value);
}

public interface IInterface
{
void Method(Derived b);
Expand Down Expand Up @@ -455,8 +508,14 @@ public interface IFoo
string Execute(string arg1, string arg2, string arg3, string arg4, string arg5, string arg6, string arg7);
string Execute(string arg1, string arg2, string arg3, string arg4, string arg5, string arg6, string arg7, string arg8);

string Execute(ref string arg1);
string Execute(ref string arg1, string arg2);

int Value { get; set; }
}

public delegate void ExecuteRHandler(ref string arg1);
public delegate void ExecuteRVHandler(ref string arg1, string arg2);
}

static class Extensions
Expand Down
Loading

0 comments on commit 2cd9690

Please sign in to comment.