cliffhanger2087
cliffhanger2087

Reputation: 47

Moq Setup reports "Expression is not a method invocation"

I planned on writing a test base for proxies/wrappers we have for a bunch of services. Each of proxy simply converts a model to TIn, calls a method on the service that returns TOut. For each of the proxy the only change is that the underlying service is different other than that its just boilerplate code inside each of the proxy.

class TestBase<TSvc, TProxy>
{
    private Mock<TSvc> svcMock = new Mock<TSvc>();
    private Mock<IMapper> mapperMock = new Mock<IMapper>();
    private TProxy proxy;

    protected void TestServiceCall<TIn, TOut>(Func<TSvc, Func<TIn, TOut>> svcCallFunc, TInModel model,
    Func<TProxy, TInModel, TOutModel> methodToTest)
    {
        var input= new TIn();
        var svcResult = new TOut();
        var proxy = ConstructProxy(this.svcMock);

        this.mapperMock.Setup(m => m.Map<TInModel, TInput>(model)).Returns(input);
        this.svcMock.Setup(m => svcCallFunc(m)(input)).Returns(svcResult);
        this.mapperMock.Setup(m => m.Map<TOut, TOutModel>(svcResult)).Returns(output);

        var result = methodToTest(model);

        ... verify if the svc was called etc. ...

        result.Should().BeSameAs(output);
    }
} 

class UserService
{
    QuestionModels GetQuestionsByUsers(SearchModel searchModel)
    { .. get all questions that comply with search parameters..}

    TopicModels GetTopicsForUsers(TopicSearchModel searchModel)
    { .. get all topics that comply with search parameters..}
}

class UserProxy
{
    private UserService service;
    private Mapper mapper;

    QuestionsClass GetQuestionsByUsers(SearchClass search)
    {
        var searchModel = mapper.Map<SearchClass, SearchModel>(search);

        var svcResult = userService.GetQuestionsByUsers(searchModel);

        return mapper.Map<QuestionModels, QuestionsClass>(svcResult);
    }

    TopicsClass GetTopicsForUsers(TopicSearchClass search)
    {
        var searchModel = mapper.Map<TopicSearchClass, TopicSearchModel>(search);

        var svcResult = userService.GetTopicsForUsers(searchModel);

        return mapper.Map<TopicModels, TopicsClass>(svcResult);
    }
}

If you can see, the number of steps that each of the method need to perform to arrive at the result is constant,

  1. map request to intermediate type
  2. call service to get result
  3. map & return result class

I have dozens of such classes that do pretty much the same. Each of them could have about 5-10 such methods.

So I wish to do the following. This way I get to save the time spent on writing boilerplate test code.

[TestClass]
class UserProxyTest : TestBase<UserService, UserProxy>
{
    [TestMethod]
    void GetQuestionsByUsersTest()
    {
        this.TestServiceCall<SearchModel, QuestionModels>(
        svc => svc.GetQuestionsByUsers,
        new SearchClass(), <-- This could be anything 
        pxy => pxy.GetQuestionsByUsers);
    }

    [TestMethod]
    void GetTopicsForUsers()
    {
        this.TestServiceCall<TopicSearchModel, TopicModels>(
        svc => svc.GetTopicsForUsers,
        new TopicSearchClass(), <-- This could be anything 
        pxy => pxy.GetTopicsForUsers);
    }   
}

The problem I got is, the line that sets up the svcMock is reporting an error that states that the expression is not a method invocation. How do I fix this problem?

Upvotes: 0

Views: 825

Answers (2)

weichch
weichch

Reputation: 10035

svcMock.Setup(m => svcCallFunc(m)(input)) is invalid.

The Setup method on Mock<T> accepts an Expression used to select a member of T to mock. The body of the expression must be either a member access expression or a method call expression to member of T. Other arbitrary expressions are invalid.

For example:

public interface IService1
{
    int Test { get; }
    void Method1();
    object Method2(object input);
}

var mock = new Mock<IService1>();

// These are valid member selectors
mock.Setup(x => x.Test);
mock.Setup(x => x.Method1());
mock.Setup(x => x.Method2(null));

// These are not
mock.Setup(x => 1);
mock.Setup(x => x.Test.GetHashCode());
mock.Setup(x => this);

To make valid call to Setup, you need either an instance of Expression<Func<TSvc, TOut>>, or an instance of Expression<Action<TSvc>>, so you can change your TestServiceCall method to:

protected void TestServiceCall<TIn, TOut>(
    Func<TIn, Expression<Func<TSvc, TOut>>> svcCallFunc)
{
    var input = new TIn();
    var output = new TOut();

    svcMock.Setup(svcCallFunc(input)).Returns(output);
}

And call it:

TestServiceCall<object, object>(input => svc => svc.Method2(input));

Upvotes: 1

Luke Woodward
Luke Woodward

Reputation: 64949

Ultimately, the error arises because the argument to .Setup() must be an expression that is accessing a property of the mock being setup or calling one of its methods. You are trying to use an expression that isn't one of these two things.

You haven't provided an example test method in a subclass of your TestBase class, but it seems reasonable to me that you want to pass the svcCallFunc parameter as a lambda referencing one of your service methods, e.g. s => s.SomeServiceMethod. If you want to to do this then you will need to declare svcCallFunc as an Expression<Func<...>> rather than a Func<...>. This expression can then be passed directly into this.svcMock.Setup(...).

You then have two choices.

Firstly, you can adjust the way you call your TestServiceCall method. Instead of passing the service method to test using s => s.SomeServiceMethod, include a Moq It.IsAny() matcher in the input argument, e.g. s => s.SomeServiceMethod(It.IsAny<TInputType>()). Your TestServiceCall is then adjusted to take an Expression<Func<TSvc, TOut>> parameter instead of an Expression<Func<TSvc, Func<TIn, TOut>>>. In this method, you can then set up the mock using

this.svcMock.Setup(svcCallFunc).Returns(value => value == input ? output : null);

(If TIn is a struct you might need to change this.)

Secondly, you could leave your calls to TestServiceCall as they are and instead pick apart the expression-tree created for s => s.SomeServiceMethod and create a new function that represents calling s.SomeServiceMethod with the input parameter, using the following somewhat-intricate code:

        if (svcCallFunc.Body is UnaryExpression unExpr
            && unExpr.Operand is MethodCallExpression instMethCall
            && instMethCall.Object is ConstantExpression constant
            && instMethCall.Method.Name == "CreateDelegate"
            && instMethCall.Arguments.Count == 2
            && constant.Value is MethodInfo methodInfo)
        {
            // The expression given appears to be something like s => s.SomeServiceMethod,
            // so create s => s.SomeServiceMethod(input) from the parts of that.
            Expression<Func<TSvc, TOut>> setupLambda = Expression.Lambda<Func<TSvc, TOut>>(
                Expression.Call(instMethCall.Arguments[1], methodInfo, Expression.Constant(input)),
                svcCallFunc.Parameters);

            this.svcMock.Setup(setupLambda).Returns(output);
        }
        else
        {
            Assert.Fail("Unable to set up mock: lambda isn't as expected");
        }

Upvotes: 1

Related Questions