Reputation: 60714
I have a interface that contains the following method signature:
TResult GetValue<T, TResult>(object key, Expression<Func<T, TResult>> property) where T : class;
Using Moq, I'm able to mock a specific call of this method like this:
var repo = new Mock<IRepository>();
repo.Setup(r => r.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId)).Returns("SecretAgentId");
Then when I do this call
repo.Object.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId);
Tt returns "SecretAgentId"
as I expect, so everything looks fine.
My problem is that in our real production code we use NSubstitute, and not Moq. I tried using the same type of setup here:
var repo = Substitute.For<ICrmRepository>();
repo.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId).Returns("SecretAgentId");
However, when I do the following call here
repo.GetValue<Customer, string>("SomeCustomerId", c => c.SecretAgentId);
It returns "" instead of "SecretAgentId"
I tried replacing c => c.SecretAgentId
with Arg.Any<Expression<Func<Customer, string>>>()
just to see if it works then, and then it returns "SecretAgentId"
as expected. But I need to verify that it is called with the correct expression, and not just any expression.
So I need to know if it is possible to get this to work in NSubstitute, and if it is, how?
Upvotes: 4
Views: 2178
Reputation: 3172
As @Fordio already pointed out, Arg.Is<T>()
is the way to go. You can use it to specify the conditions an argument must meet in order to provide the expected result. It is especially useful, when you don't have control of the argument's initialization and can't provide a proper reference to Returns()
that would be valid for later method calls.
For instance you want to mock following interface:
public interface IQueryCache
{
TResult GetOrExecute<TQuery, TResult>(TQuery query, Func<TResult> func)
where TQuery : IQuery<TResult>
where TResult : class;
}
Which is used by another class that initializes the parameters of GetOrExecute()
by itself:
public class RequestHandler
{
private readonly IQueryCache _cache;
private readonly IRepository _repository;
public RequestHandler(IRepository repository, IQueryCache cache)
{
_repository = repository;
_cache = cache;
}
public TResult GetResult(Guid userId)
{
var query = new Query() { UserId = userId };
var result = _cache.GetOrExecute(query, () => _repository.GetUserItems(query));
return result;
}
}
In consequence following setup would not work, if you wanted to mock the cache and test only the RequestHandler
, because you can't provide a reference to the parameter, which is initialized within GetRestult()
:
var repository = Substitute.For<IRepository>();
var cache = Substitute.For<IQueryCache>();
repository.GetUserItems(query).Returns(expected);
cache.GetOrExecute(query, () => repository.GetUserItems(query)).Returns(expected);
var handler = new RequestHandler(repository, cache);
However you can work around this by specifying conditions on the argument's properties like this:
cache.GetOrExecute(Arg.Is<Query>(q => q.UserId == "value"), Arg.Any<Func<IEnumerable<TResult>>>()).Returns(expected);
Upvotes: 0
Reputation: 3820
I can't remember exact syntax, so forgive me if this isn't A1 correct, and it is a bit kludgy but...
I believe you were on the right track when you tried Arg.Any, however try using Arg.Is like this:
Arg.Is<Expression<Func<Customer, string>>>(x => {
var m = ((Expression)x).Body as MemberExpression;
var p = m.Member as PropertyInfo;
return p.Name == "SecretAgentId";
});
Upvotes: 2
Reputation: 14962
I think that expressions are evaluated in NSubstitute depending on their closure scope, so the two expressions declarations are not identical. It looks like a bug to me, you may want to open an issue.
You can however take the expression out of the substitution declaration and it works correctly:
private static void Main(string[] args)
{
Expression<Func<string, string>> myExpression = s => s.Length.ToString();
var c = Substitute.For<IRepo>();
c.GetValue<string, string>("c", myExpression).Returns("C");
var result = c.GetValue<string, string>("c", myExpression); // outputs "C"
}
Upvotes: 5