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

Validate delegates passed to Returns more strictly #520

Merged
merged 5 commits into from
Nov 10, 2017
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1
#### Changed

* **Breaking change:** `SetupSequence` now overrides pre-existing setups like all other `Setup` methods do. This means that exhausted sequences no longer fall back to previous setups to produce a "default" action or return value. (@stakx, #476)
* Delegates passed to `Returns` are validated a little more strictly than before (return type and parameter count must match with method being set up) (@stakx, #520)

#### Fixed

Expand Down
121 changes: 121 additions & 0 deletions Moq.Tests/ReturnsValidationFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.Reflection;

using Moq.Language.Flow;

using Xunit;

namespace Moq.Tests
{
public class ReturnsValidationFixture
{
private Mock<IType> mock;
private ISetup<IType, IType> setup;

public ReturnsValidationFixture()
{
this.mock = new Mock<IType>();
this.setup = this.mock.Setup(m => m.Method(It.IsAny<object>()));
}

[Fact]
public void Returns_does_not_accept_delegate_without_return_value()
{
Action<object> delegateWithoutReturnValue = (arg) => { };

var ex = Record.Exception(() =>
{
this.setup.Returns(delegateWithoutReturnValue);
});

Assert.IsType<ArgumentException>(ex);
}

[Fact]
public void Returns_does_not_accept_delegate_with_wrong_return_type()
{
Func<string> delegateWithWrongReturnType = () => "42";

var ex = Record.Exception(() =>
{
this.setup.Returns(delegateWithWrongReturnType);
});

Assert.IsType<ArgumentException>(ex);
}

[Fact]
public void Returns_accepts_parameterless_delegate_even_for_method_having_parameters()
{
Func<IType> delegateWithoutParameters = () => default(IType);
this.setup.Returns(delegateWithoutParameters);

var ex = Record.Exception(() =>
{
this.mock.Object.Method(42);
});

Assert.Null(ex);
}

[Fact]
public void Returns_does_not_accept_delegate_with_wrong_parameter_count()
{
Func<object, object, IType> delegateWithWrongParameterCount = (arg1, arg2) => default(IType);

var ex = Record.Exception(() =>
{
this.setup.Returns(delegateWithWrongParameterCount);
});

Assert.IsType<ArgumentException>(ex);
}

[Fact]
public void Returns_accepts_delegate_with_wrong_parameter_types_but_setup_invocation_will_fail()
{
Func<string, IType> delegateWithWrongParameterType = (arg) => default(IType);
this.setup.Returns(delegateWithWrongParameterType);

var ex = Record.Exception(() =>
{
mock.Object.Method(42);
});

Assert.IsType<ArgumentException>(ex);

// In case you're wondering why this use case isn't "fixed" by properly validating delegates
// passed to `Returns`... it's entirely possible that some people might do this:
//
// mock.Setup(m => m.Method(It.IsAny<int>()).Returns<int>(obj => ...);
// mock.Setup(m => m.Method(It.IsAny<string>()).Returns<string>(obj => ...);
//
// where `Method` has a parameter of type `object`. That is, people might rely on a matcher
// to ensure that the return callback delegate invocation (and the cast to `object` that has to
// happen) will always succeed. See also the next test, as well as old Google Code issue 267
// in `IssueReportsFixture.cs`.
//
// While not the cleanest of techniques, it might be useful to some people and probably
// shouldn't be broken by eagerly validating everything.
}

[Fact]
public void Returns_accepts_delegate_with_wrong_parameter_types_and_setup_invocation_will_succeed_if_args_convertible()
{
Func<string, IType> delegateWithWrongParameterType = (arg) => default(IType);
this.setup.Returns(delegateWithWrongParameterType);

var ex = Record.Exception(() =>
{
mock.Object.Method(null);
});

Assert.Null(ex);
}

public interface IType
{
IType Method(object arg);
}
}
}
4 changes: 2 additions & 2 deletions Source/MethodCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,14 @@ public string FailMessage
set => this.failMessage = value;
}

public MethodInfo Method => this.method;

bool IProxyCall.IsConditional => this.condition != null;

bool IProxyCall.IsVerifiable => this.verifiable;

bool IProxyCall.Invoked => this.callCount > 0;

MethodInfo IProxyCall.Method => this.method;

Expression IProxyCall.SetupExpression => this.originalExpression;

[Conditional("DESKTOP")]
Expand Down
39 changes: 39 additions & 0 deletions Source/MethodCallReturn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
using Moq.Properties;
using Moq.Proxy;
using System;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;

Expand Down Expand Up @@ -157,10 +158,48 @@ public IReturnsResult<TMock> CallBase()

private void SetReturnDelegate(Delegate value)
{
if (value != null)
{
ValidateReturnDelegate(value);
}

this.valueDel = value;
this.returnValueKind = ReturnValueKind.Explicit;
}

private void ValidateReturnDelegate(Delegate callback)
{
var callbackMethod = callback.GetMethodInfo();

var actualParameters = callbackMethod.GetParameters();
if (actualParameters.Length > 0)
{
var expectedParameters = this.Method.GetParameters();
if (actualParameters.Length != expectedParameters.Length)
{
throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture,
Resources.InvalidCallbackParameterCountMismatch,
expectedParameters.Length,
actualParameters.Length));
}
}

var expectedReturnType = this.Method.ReturnType;
var actualReturnType = callbackMethod.ReturnType;

if (!expectedReturnType.IsAssignableFrom(actualReturnType))
{
throw new ArgumentException(
string.Format(
CultureInfo.CurrentCulture,
Resources.InvalidCallbackReturnTypeMismatch,
expectedReturnType,
actualReturnType));
}
}

protected override void SetCallbackWithoutArguments(Action callback)
{
if (this.ProvidesReturnValue())
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.

6 changes: 6 additions & 0 deletions Source/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -365,4 +365,10 @@ Expected invocation on the mock once, but was {4} times: {1}</value>
<data name="ProtectedMemberNotFound" xml:space="preserve">
<value>Type {0} does not have matching protected member: {1}</value>
</data>
<data name="InvalidCallbackReturnTypeMismatch" xml:space="preserve">
<value>Invalid callback. Setup on method with return type ({0}) cannot invoke callback with return type ({1}).</value>
</data>
<data name="InvalidCallbackParameterCountMismatch" xml:space="preserve">
<value>Invalid callback. Setup on method with {0} parameter(s) cannot invoke callback with different number of parameters ({1}).</value>
</data>
</root>