Reputation: 600
Could someone explain to me, why this doesn't work?
builder.Setup(b => b.BuildCommand(query ?? It.IsAny<string>())).Returns(command);
If query
is null
, BuildCommand
will be passed null
, and not It.IsAny<string>()
Instead, I have to do this:
if(query == null)
builder.Setup(b => b.BuildCommand(It.IsAny<string>())).Returns(command);
else
builder.Setup(b => b.BuildCommand(query)).Returns(command);
Is it related to the delegate?
EDIT - Complete example
public static void ReturnFromBuildCommand(this Mock<IQueryCommandBuilder> builder, IQueryCommand command, string query = null)
{
if(query == null)
builder.Setup(b => b.BuildCommand(It.IsAny<string>())).Returns(command);
else
builder.Setup(b => b.BuildCommand(query)).Returns(command);
}
Then I can call it like
var command = new Mock<IQueryCommand>();
var builder = new Mock<IQueryCommandBuilder>();
builder.ReturnFromBuildCommand(command.Object);
Or
string query = "SELECT Name FROM Persons;";
builder.ReturnFromBuildCommand(command.Object, query);
Depending on whether I care about the parameter or not.
Upvotes: 3
Views: 727
Reputation: 4607
The Setup
method of the mock takes in an expression, which the Moq framework then deconstructs to determine the method being called and the arguments to it. It then sets up an interceptor to match the arguments.
You can see this in the Mock
source:
internal static MethodCallReturn<T, TResult> Setup<T, TResult>(
Mock<T> mock,
Expression<Func<T, TResult>> expression,
Condition condition)
where T : class
{
return PexProtector.Invoke(() =>
{
if (expression.IsProperty())
{
return SetupGet(mock, expression, condition);
}
var methodCall = expression.GetCallInfo(mock);
var method = methodCall.Method;
var args = methodCall.Arguments.ToArray();
ThrowIfNotMember(expression, method);
ThrowIfCantOverride(expression, method);
var call = new MethodCallReturn<T, TResult>(mock, condition, expression, method, args);
var targetInterceptor = GetInterceptor(methodCall.Object, mock);
targetInterceptor.AddCall(call, SetupKind.Other);
return call;
});
}
Here args
is of type Expression[]
.
(reference: https://github.com/moq/moq4/blob/master/Source/Mock.cs#L463)
This args
array is passed into the constructor for the Moq type MethodCallReturn
as the parameter arguments
. That constructor (via the base class MethodCall
) generates an argument matcher using MatcherFactory.Create
. (reference: https://github.com/moq/moq4/blob/master/Source/MethodCall.cs#L148)
This is where things start to get interesting!
In the MatcherFactory.Create method, it tries to determine the type of the argument's Expression by looking that the Expression.NodeType
and/or checking it against known types, such as MatchExpression
(which is what something like Is.Any<string>()
would be).
(reference: https://github.com/moq/moq4/blob/master/Source/MatcherFactory.cs#L54)
So let's take a step back. In your specific case, the code query ?? Is.Any<string>()
is compiled down to an expression itself -- something like this ugly mess (as generated by the dotPeek decompiler):
(Expression) Expression.Coalesce((Expression) Expression.Field((Expression) Expression.Constant((object) cDisplayClass00, typeof (Extension.\u003C\u003Ec__DisplayClass0_0)),
FieldInfo.GetFieldFromHandle(__fieldref (Extension.\u003C\u003Ec__DisplayClass0_0.query))),
(Expression) Expression.Call((Expression) null, (MethodInfo) MethodBase.GetMethodFromHandle(__methodref (It.IsAny)), new Expression[0]))
And that's what the first argument looks like. You can rewrite your code to better express what Moq sees, like this:
public static void ReturnFromBuildCommand(this Mock<IQueryCommandBuilder> builder, IQueryCommand command, string query = null)
{
Expression<Func<IQueryCommandBuilder, IQueryCommand>> expressOfFunc = commandBuilder => (commandBuilder.BuildCommand(query ?? It.IsAny<string>()));
var methodCall = expressOfFunc.Body as MethodCallExpression;
var args = methodCall.Arguments.ToArray();
var nodeType = args[0].NodeType;
builder.Setup(expressOfFunc)
.Returns(command);
}
If you place a breakpoint, you can see that the value of nodeType
is Coalesce
. Now, go back and change it to just use query
, and nodeType
becomes MemberAccess
. Use It.IsAny<string>()
, and nodeType
is Call
.
This explains the differences between the three approaches and why it's not acting like you expected. As for why it triggers on null
is not clear to me, to be honest, but whatever matcher comes out of MatcherFactory.CreateMatcher
seems to think null
is a valid value for your mock configuration.
Upvotes: 6