-
Notifications
You must be signed in to change notification settings - Fork 266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve ForPartsOf to use interface and implementation class instead of class only #625
Comments
Hi @wsaeed , |
Hi @dtchepak
In the above example I have to declare TargetImpl.SaveTarget as virtual for mocking. It will be really nice if I can do something similar to
Let me know if you need more clarification |
For example, calling wrapped methods in FakeItEasy (https://fakeiteasy.readthedocs.io/en/stable/calling-wrapped-methods/). |
Thanks for the example @wsaeed. Is it sufficient to configure this forwarding using var mock = Substitute.For<ITarget>();
var impl = new TargetImpl();
mock.SaveTarget(null).ReturnsForAnyArgs(x => impl.SaveTarget(x.Arg<string>())); @oldmine Thank you for the FIE example. This is a little trickier in NSubstitute due to the syntax. We would need to use |
Not sure if this what you are looking for, but this may be a workaround if you want to be able to get a partial for a class that implements an interface, where interface members will be routed to the class implementations (which do not have to be virtual - just not explicit implementations, as these will be hidden by the mock) If you look at the method public static T ForPartsOf<T>(params object[] constructorArguments) where T : class => (T) SubstitutionContext.Current.SubstituteFactory.CreatePartial(new Type[1]
{
typeof (T)
}, constructorArguments); With some research, I found something like this to forward all calls from a substitute to another object: public static class SubstituteExtensions
{
public static T ForwardCallsTo<T>(this T subst, object instance)
{
SubstitutionContext.Current.GetCallRouterFor(subst)
.RegisterCustomCallHandlerFactory(state => new RedirectToInstanceHandler(instance));
return subst;
}
private class RedirectToInstanceHandler : ICallHandler
{
private readonly object _instance;
public RedirectToInstanceHandler(object instance)
{
_instance = instance;
}
public RouteAction Handle(ICall call)
{
// Look if the called method is implemented on the instance.
var methodInfo = call.GetMethodInfo();
MethodInfo methodOnInstance = null;
if (methodInfo.ReflectedType.IsInterface)
{
if (call.Target() != _instance // prevent stackoverflow caused by forwarding to itself
&& methodInfo.ReflectedType.IsInstanceOfType(_instance))
methodOnInstance = methodInfo; // otherwise, forward to interface implementation on other object
} else // find implementing method called from the class
methodOnInstance = GetInterfaceDeclarationsForMethod(methodInfo).FirstOrDefault(i => i.DeclaringType.IsInstanceOfType(_instance));
if (methodOnInstance != null) // If so, forward the call to the corresponding method on the instance.
return RouteAction.Return(methodOnInstance.Invoke(_instance, call.GetArguments()));
return RouteAction.Continue(); // If not, do nothing. (fallback to default)
}
}
private static IEnumerable<InterfaceMapping> GetAllInterfaceMaps(Type aType) =>
aType.GetTypeInfo()
.ImplementedInterfaces
.Select(aType.GetInterfaceMap);
private static IEnumerable<MethodInfo> GetInterfaceDeclarationsForMethod(MethodInfo mi) =>
GetAllInterfaceMaps(mi.ReflectedType)
.SelectMany(map => Enumerable.Range(0, map.TargetMethods.Length)
.Where(n => map.TargetMethods[n] == mi)
.Select(n => map.InterfaceMethods[n]));
} Note it will not forward calls that are implemented as explicit if you let the substitute interface forward to itself: var subst = (IList<int>)SubstitutionContext.Current.SubstituteFactory.CreatePartial(new[] { typeof(IList<int>), typeof(List<int>) }); // now we have a substitute inherited from List<int> and able to intercept IList<int>, though IList<> calls won't be forwarded to List<int> implementation (yet).
subst.ForwardCallsTo(subst); // here we make sure it does, using the custom helper, and we can still inspect received calls etc. as long as calls are made through the interface and not the class. (as most of the class members are not virtual)
// some we still need to add by hand if we need them as they are implemented explicitly by List<int>
((IEnumerable)subst).GetEnumerator().Returns(_ => ((List<int>)subst).GetEnumerator());
subst.GetEnumerator().Returns(_ => ((List<int>)subst).GetEnumerator()); It can also be used like this if your substitute does not have to be a subclass of a certain class, but just forwarding calls is enough (for example when testing code that does not rely on the object being of type List, but IList instead) - Explicit implemented members will just get forwarded here, as the target object is not the same object as the mock. This way can even have one substitute forward members from multiple interfaces each to different objects that implement them. var wrapped = new List<int> {1,2,3};
var subst = Subsitute.For<IList<int>>();
subst.ForwardCallsTo(wrapped); |
Hi! I've sent you a pull request implementing this feature (#700). I hope you find it useful. |
This one is fixed by #700, isn't? (Released with 5.3.0) |
I have tested this, but there is an exception thrown if public interface IMyInterface : IReadOnlyList<int> { }
var myProxy = Substitute.ForTypeForwardingTo<IMyInterface, List<int>>(); // <= throws
/*
NSubstitute.Exceptions.CanNotForwardCallsToClassNotImplementingInterfaceException
The provided class 'List`1' doesn't implement all requested interfaces.
at NSubstitute.Proxies.CastleDynamicProxy.CastleDynamicProxyFactory.VerifyClassImplementsAllInterfaces(Type classType, IEnumerable`1 additionalInterfaces)
at NSubstitute.Proxies.CastleDynamicProxy.CastleDynamicProxyFactory.CreatePartialProxy(Type typeToProxy, Type[] additionalInterfaces, Object[] constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, Boolean isPartial)
at NSubstitute.Proxies.CastleDynamicProxy.CastleDynamicProxyFactory.GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[] additionalInterfaces, Boolean isPartial, Object[] constructorArguments)
at NSubstitute.Core.SubstituteFactory.Create(Type[] typesToProxy, Object[] constructorArguments, Boolean callBaseByDefault, Boolean isPartial)
at NSubstitute.Core.SubstituteFactory.CreatePartial(Type[] typesToProxy, Object[] constructorArguments)
at NSubstitute.Substitute.ForTypeForwardingTo[TInterface,TClass](Object[] constructorArguments)
*/ I think it should also work if the object does not implement all interface (directly), but also if interfaces are indirectly implemented or the class only implements a subset if the interfaces inherited by the interface (if any, and maybe throws only if there are none), leaving members not covered behave the same as a 'normal' substitute. As workaround for now I use the custom extension I made earlier (#625 (comment)): var realInstance = new List<int>();
var myProxy = Substitute.For<IMyInterface>();
myproxy.ForwardCallsTo(realInstance); |
Hi!
This sounds like a good idea for a new Github issue. Don't get me wrong but the original request has been implemented and it's something hard to track the exact requirements/status of a long Github discussion. |
Yes, I understand changing a feature that is already released often involves regression/backwards compatibility issues, so new behavior should be added with care and code relying on existing behavior in mind. I'll create a new issue for it. I can't say when/if I have to time suggest an implementation, but I will when I find time. |
Currently
ForPartsOf
accepts a single template parameter (a class), which requires the mocked method to be virtual. Making methods of a class virtual allows derived classes to override which may change the behavior of the class.TImplementation Substitute.For<TImplementation>(...)
https://nsubstitute.github.io/help/partial-subs/
Suggestion is to to have an overload similar to
TInterface Substitute.For<TInterface, TImplementation>(...)
Any methods which are not mocked forwarded to the implementation class.
The text was updated successfully, but these errors were encountered: