Reputation: 47
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,
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
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
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