Since I've taken a stab at this myself I figured I should post some of what I've come up with so far. Below are a set of types that allow applying a broad default value capability to individual NSubstitute substitute instances.
public interface IDefaultValueFactory
{
T GetDefault<T>();
}
public static class NSubstituteDefaultValueConfigurator
{
public static void Configure(Type substituteType, object substitute, IDefaultValueFactory valueFactory)
{
var type = typeof(NSubstituteDefaultValueConfigurator<>)
.MakeGenericType(substituteType);
var configurator = type
.GetConstructor(new Type[] { typeof(IDefaultValueFactory) })
.Invoke(new object[] { valueFactory });
type.GetMethod("ConfigureDefaultReturnValuesForAllMethods")
.Invoke(configurator, new object[] { substitute });
}
}
public class NSubstituteDefaultValueConfigurator<T>
{
private readonly IDefaultValueFactory _valueFactory;
public NSubstituteDefaultValueConfigurator(IDefaultValueFactory valueFactory)
{
_valueFactory = valueFactory;
}
private object GetDeafultValue<TResult>()
{
return _valueFactory.GetDefault<TResult>();
}
public void ConfigureDefaultReturnValuesForAllMethods(T substitute)
{
var interfaces = substitute
.GetType()
.GetInterfaces()
// HACK: Specifically exclude supporting interfaces from NSubstitute
.Where(i =>
i != typeof(Castle.DynamicProxy.IProxyTargetAccessor) &&
i != typeof(ICallRouter) /*&&
i != typeof(ISerializable)*/)
.ToArray();
var methods = interfaces
.SelectMany(i => i.GetMethods())
.Where(m => m.ReturnType != typeof(void))
// BUG: skipping over chained interfaces in NSubstitute seems
// to cause an issue with embedded returns. Using them however
// causes the mock at the end or along a chained call not to be
// configured for default values.
.Where(m => !m.ReturnType.IsInterface);
foreach (var method in methods)
{
var typedConfigureMethod = this
.GetType()
.GetMethod("ConfigureDefaultReturnValuesForMethod", BindingFlags.NonPublic | BindingFlags.Static)
.MakeGenericMethod(method.ReturnType);
var defaultValueFactory = new Func<CallInfo, object>(
callInfo => this
.GetType()
.GetMethod("GetDeafultValue", BindingFlags.NonPublic | BindingFlags.Instance)
.MakeGenericMethod(method.ReturnType)
.Invoke(this, null));
typedConfigureMethod.Invoke(
this,
new object[]
{
substitute,
defaultValueFactory,
method
});
}
//var properties = interfaces.SelectMany(i => i.GetProperties());
var properties = substitute
.GetType().GetProperties();
foreach (var property in properties)
{
var typedConfigureMethod = this
.GetType()
.GetMethod("ConfigureDefaultReturnValuesForProperty", BindingFlags.NonPublic | BindingFlags.Static)
.MakeGenericMethod(property.PropertyType);
var defaultValueFactory = new Func<CallInfo, object>(
callInfo => this
.GetType()
.GetMethod("GetDeafultValue", BindingFlags.NonPublic | BindingFlags.Instance)
.MakeGenericMethod(property.PropertyType)
.Invoke(this, null));
typedConfigureMethod.Invoke(
this,
new object[]
{
substitute,
defaultValueFactory,
property
});
}
}
private static void ConfigureDefaultReturnValuesForMethod<TResult>(
T substitute,
Func<CallInfo, object> defaultValueFactory,
MethodInfo method)
{
var args = method
.GetParameters()
.Select(p => GetTypedAnyArg(p.ParameterType))
.ToArray();
// Call the method on the mock
var substituteResult = method.Invoke(substitute, args);
var returnsMethod = typeof(SubstituteExtensions)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.First(m => m.GetParameters().Count() == 2)
.MakeGenericMethod(method.ReturnType);
var typedDefaultValueFactory = new Func<CallInfo, TResult>(callInfo => (TResult)defaultValueFactory(callInfo));
returnsMethod.Invoke(null, new[] { substituteResult, typedDefaultValueFactory });
}
private static void ConfigureDefaultReturnValuesForProperty<TResult>(
T substitute,
Func<CallInfo, object> defaultValueFactory,
PropertyInfo property)
{
// Call the property getter on the mock
var substituteResult = property.GetGetMethod().Invoke(substitute, null);
var returnsMethod = typeof(SubstituteExtensions)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.First(m => m.GetParameters().Count() == 2)
.MakeGenericMethod(property.PropertyType);
var typedDefaultValueFactory = new Func<CallInfo, TResult>(callInfo => (TResult)defaultValueFactory(callInfo));
returnsMethod.Invoke(null, new[] { substituteResult, typedDefaultValueFactory });
}
private static object GetTypedAnyArg(Type argType)
{
return GetStaticGenericMethod(typeof(Arg), "Any", argType);
}
private static MethodInfo GetStaticGenericMethod(
Type classType,
string methodName,
params Type[] typeParameters)
{
var method = classType
.GetMethod(methodName, BindingFlags.Static | BindingFlags.Public)
.MakeGenericMethod(typeParameters);
return method;
}
}
Since the Configure method is required to be called for each individual substitute instance there either needs to be be some intrusive modification of the supporting classes in the AutoFixture AutoNSubstitute supporting classes or a replacement implementation of AutoNSubstitute needs to be provided. In my tinkering directly within the AutoNSubstitute source code I modified the NSubstituteBuilder class as follows to adapt it to have configurable default/auto value capability.
public object Create(object request, ISpecimenContext context)
{
if (!SubstitutionSpecification.IsSatisfiedBy(request))
return new NoSpecimen(request);
var substitute = Builder.Create(request, context);
if (substitute == null)
return new NoSpecimen(request);
NSubstituteDefaultValueConfigurator.Configure(
substitute.GetType(),
substitute,
new AutoFixtureDefaultValueFactory(context));
return substitute;
}
private class AutoFixtureDefaultValueFactory : IDefaultValueFactory
{
private readonly ISpecimenContext _context;
public AutoFixtureDefaultValueFactory(ISpecimenContext context)
{
_context = context;
}
public T GetDefault<T>()
{
return _context.Create<T>();
}
}
Unfortunately there is either a bug in my implementation that handles reflection calls to property getters on substitutes or NSubstitute has a difference of handling properties than methods but I've run into a bit of a roadblock either way. The remaining issue is that with chained interfaces (interfaces that return other interfaces from their members) that a CouldNotSetReturnException is thrown when a concrete class that should be resolved through AutoFixture is encountered on a leaf property call. This only appears to happen for properties and not methods though which is both interesting and unfortunate. Given what appears to be a limitation in NSubsitute Returns method design and also a limitation in the general API for configuring default values more broadly.
So at this point it appears that the answer is no, out of the box the AutoNSubstitute customization for AutoFixture does not support the capability of returning the same automatic values returned by the fixture through the members of the returned substitutes. On the other hand, it appears that the maintainer of AutoFixture is willing to accept and perhaps support a reasonable implementation of this feature and I've been able to show that I can achieve at least a partially working implementation using the available facilities of NSubstitute without modification.
As a side note, a pattern that seems to be obvious to me is that mocking libraries that use static factories to create mocks and that do not have any type of instance based context naturally lack the ability to configure behavior of generated mocks per test. I thought about this limitation early on when first adopting mocks within unit tests and this is the first time it appears to be causing a problem for me.