-
Notifications
You must be signed in to change notification settings - Fork 264
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
Feature: Extend PartsOf to mock non-virtual methods implementing an i… #700
Conversation
Nice feature to test the examples in the documentation. 😄 There was an use case in the docs that was not in the code, in the file There was a regression because the new ForPartsOf is based on For<>, and there was a new check to ensure that the class implemented the interface. It did not break any of the existing tests but it did with those on the documentation. I have solved it and copied the test into the solution. Now |
302440e
to
a04dc32
Compare
I've found that the use case mentioned in my previous comment and taken from the docs was not working ok for all scenarios. In the case of partial substitutes, the substitute can't extend the base class, just the interface, since we are substituting non-virtual methods. I think this is an acceptable limitation. In my previous implementation, regretfully, this was happening for all other substitutes. While I believe it is an edge case, it would have meant a breaking change. The new implementation involves passing a parameter around to know when we are creating a partial substitute or not. I find this a bit inelegant, but it causes less friction with the current code. I've removed the previous commits and replaced them with a squashed version. Relevant tests are in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay in reviewing this. 😓 🙇
This is a really nice PR, thanks! I'm wondering if, to be extra careful, we should leave the existing Substitute.ForPartsOf
unchanged, and expose this functionality through Substitute.ForTypeForwardingTo()
or similar? That way changes to the types passed will not result in significant changes to how the resulting substitute behaves. WDYT?
src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
Outdated
Show resolved
Hide resolved
src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
Outdated
Show resolved
Hide resolved
Hi!
Thank you for taking the time to review the PR. I'm glad you like it. I
will make the amendments you suggested.
|
Hi again I've added the changes you suggested:
Please, kindly let me know if you find something else to improve. |
I would love to have this integrated in NSubstitute... @marcoregueira - thank you for your work on this! |
Thanks @marcoregueira for your solution, it's something I look forward to have in NSubstitute. |
Sorry, this completely slipped off my radar. 😞 🙇 Before I merged I wanted to add some tests to demonstrate equivalence between substitutes created using this method vs. If anyone has time to look at that I would greatly appreciate it, otherwise thanks a lot for the reminder and I'll try to look into this again shortly. |
e88249f
to
05cb82c
Compare
Thank you for your review. I've upgraded the code to the latest version and made the necessary updates to the PR. To verify the new feature, please refer to the tests in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great. Couple of very minor nitpicks to resolve then I'll merge 👍
src/NSubstitute/Substitute.cs
Outdated
@@ -89,4 +90,19 @@ public static T ForPartsOf<T>(params object[] constructorArguments) | |||
var substituteFactory = SubstitutionContext.Current.SubstituteFactory; | |||
return (T)substituteFactory.CreatePartial([typeof(T)], constructorArguments); | |||
} | |||
|
|||
public static T ForTypeForwardingTo<T,TClass>(params object[] constructorArguments) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chore: could you please add doc comments here (see ForPartsOf
and other public methods for examples).
src/NSubstitute/Substitute.cs
Outdated
//public static T ForTypeForwardingTo<T, T2, T3>(params object[] constructorArguments) | ||
// where T : class | ||
//{ | ||
// var substituteFactory = SubstitutionContext.Current.SubstituteFactory; | ||
// return (T)substituteFactory.CreatePartial(new[] { typeof(T), typeof(TClass) }, constructorArguments); | ||
//} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chore (nitpick): please remove commented out code.
src/NSubstitute/Substitute.cs
Outdated
var substituteFactory = SubstitutionContext.Current.SubstituteFactory; | ||
return (T)substituteFactory.CreatePartial([typeof(T)], constructorArguments); | ||
} | ||
|
||
public static T ForTypeForwardingTo<T,TClass>(params object[] constructorArguments) | ||
where T : class | ||
{ | ||
var substituteFactory = SubstitutionContext.Current.SubstituteFactory; | ||
return (T)substituteFactory.CreatePartial(new[] { typeof(T), typeof(TClass) }, constructorArguments); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: not sure if it is just the diff view but the code indenting looks non-standard here?
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: additional blank lines not required
@@ -1,4 +1,6 @@ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question (nitpick): several of these changed files have a blank line to added to start. Is this an editorconfig thing or accidental?
…ods or sealed classes implementing an interface. How to use: var substitute = Substitute.ForTypeForwardingTo <ISomeInterface,SomeImplementation>(argsList); In this case, it doesn't matter if methods are virtual or not; it will intercept all calls since we will be working with an interface all the time. For Limitations: Overriding virtual methods effectively replaces its implementation both for internal and external calls. With this implementation Nsubstitute will only intercept calls made by client classes using the interface. Calls made from inside the object itself to it's own method, will hit the actual implementation.
266ace9
to
eed5290
Compare
Hi @dtchepak I found some time to address the issues you raised. Thank you for reviewing them. I've included the documentation section you requested. Please let me know if the wording needs further adjustment. Feel free to let me know if you need any more changes! Changes:
|
public sealed class CanNotForwardCallsToClassNotImplementingInterfaceException(Type type) : TypeForwardingException(DescribeProblem(type)) | ||
{ | ||
private static string DescribeProblem(Type type) | ||
{ | ||
return string.Format("The provided class '{0}' doesn't implement all requested interfaces. ", type.Name); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): for a future change, I think it would be helpful for this to explain which of the requested interfaces has not been implemented. If we see "Class 'A' does not implement all requested interfaces. To support call forwarding 'A' must implement 'B', 'C', and 'D'." then it gives us a good hint on how to fix the exception.
{ | ||
private static string DescribeProblem(Type type) | ||
{ | ||
return string.Format("The provided class '{0}' is abstract. ", type.Name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick (non-blocking): trailing space: "...abstract . "
vs "...abstract ."
..
!castleInvocation.MethodInvocationTarget.IsFinal) | ||
!castleInvocation.MethodInvocationTarget.IsAbstract) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: dropping "not final" constraint here is required to support call forwarding?
/// <summary> | ||
/// Creates a proxy for a class that implements an interface, forwarding methods and properties to an instance of the class, effectively mimicking a real instance. | ||
/// Both the interface and the class must be provided as parameters. | ||
/// The proxy will log calls made to the interface members and delegate them to an instance of the class. Specific members can be substituted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick (non-blocking): "will log calls" -> "will intercept calls"
Thanks @marcoregueira ! |
Hi
I was in need of a feature allowing PartsOf to mock non-virtual methods implementing an interface, just the same as requested in #625.
I decided to tinker a bit to see If there was any way of doing it. Solution came out faster than expected and here are the results. I thought you might find it useful enough to include it.
In short, Castle already provides this feature and the task was, mostly, opening a way to expose it without breaking anything.
How to use:
var substitute = Substitute.ForPartsOf<ISomeInterface,SomeImplementation>(argsList);
var substitute = Substitute.ForTypeForwardingTo<ISomeInterface,SomeImplementation>(argsList);
Tests are included, of course, to see it in action.
Obvious limitations:
Overriding virtual methods effectively replaces its implementation both for internal and external calls. With this implementation Nsubstitute will only intercept calls made by client classes using the interface. Calls made from inside the object itself to its own method will hit the actual implementation.
Possible improvements
The next logical improvement is to provide mocking for already created objects conforming to an interface. Something like this:
Please kindly take a look and give your feedback! Thanks!