Handling Method Parameter Default Values Using Moq

We at Contigo often use a pattern where methods with multiple nullable parameters retrieve collections of entities from a data store a bit like this:

IEnumerable<Thing> GetThingsFromDatabase(int? intFilter = null, bool? boolFilter = null, stringFilter = null)
{
...
}

These methods treat a null value as “no filtering required” so they can be called with only those filters that are relevant to the calling code. In this way we can keep database querying and object instantiation logic in a single place while our business logic specifies only those filters that it cares about. A typical line of business logic code might look like this:

var things = datamapper.GetThingsFromDatabase(boolFilter: true);

When it comes to unit testing our business logic code, we sometimes have an issue though. We use Moq to mock out our data mappers, and many of our tests look like this:

var mock = new Mock<IDataMapper>();
mock.Setup(mapper => mapper.GetThingsFromDatabase(It.IsAny<int?>(), true, It.IsAny<string>());
...

While optional parameters help de-couple our business logic from the filter parameters that it doesn’t care about* the same cannot be said for our unit tests. Adding a new parameter to our data mappers could break a lot of tests; all because the following is not allowed by C# Expressions:

var mock = new Mock<IDataMapper>();
mock.Setup(mapper => mapper.GetThingsFromDatabase(boolFilter: true));
...

As a way around this, we have implemented an extension method for Moq as follows:

static class MoqExtensions
{
public static ISetup<T, TResult> Setup<T, TResult>(this Mock<T> mock, string methodName, Dictionary<string, object> specifiedParameters) where T : class
{
// find the method on the mocked type that matches the name of the method we're mocking
var matchingMethods = typeof(T).GetMethods().Where(m => m.Name == methodName);
foreach (var matchingMethod in matchingMethods)
{
var matchingMethodParameters = matchingMethod.GetParameters();
// Check all of the specified parameters exist on this matchingMethod, otherwise continue to the next matching method
if (specifiedParameters.Count > 0 && specifiedParameters.All(sp => matchingMethodParameters.Any(p => p.Name == sp.Key) == false))
continue;
var defaultParameters = new List<Expression>();
// For each of the matching method parameters, create an It.IsAny<type>() expression unless the caller has specified a constant value for this paramter
foreach (ParameterInfo methodParam in matchingMethodParameters)
{
if (specifiedParameters.ContainsKey(methodParam.Name))
defaultParameters.Add(Expression.Constant(specifiedParameters[methodParam.Name], methodParam.ParameterType));
else
defaultParameters.Add(Expression.Call(typeof(It), "IsAny", new Type[] { methodParam.ParameterType }, null));
}

var methodCall = Expression.Call(Expression.Parameter(typeof(T), "mock"), matchingMethod, defaultParameters);
var expr = Expression.Lambda<Func<T, TResult>>(methodCall, Expression.Parameter(typeof(T), "mock"));
return mock.Setup(expr);
}
return null;
}
}

This can be called like this:

var mock = new Mock<IDataMapper>();
mock.Setup<IDataMapper, IEnumerable<Thing>>("GetThingsFromDatabase", new Dictionary() { { "boolFilter", true } });
...

Which is about as close as I think it’s possible to get to what we’re really after.

 

*Technically, the default parameter values are implemented on the caller’s side not the callee’s side so any code that calls our data mappers must be re-complied if we add a parameter. But as the code that calls them is within the same solution this is not an issue for us.