Skip to content
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

Merged
merged 1 commit into from
Aug 11, 2024

Conversation

marcoregueira
Copy link
Contributor

@marcoregueira marcoregueira commented Jul 21, 2022

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:

ISomeInterface  someObject = new SomeClass();
ISomeInterface substitute = Substitute.ForInstanceForwardingTo<ISomeInterface>(someObject);

Please kindly take a look and give your feedback! Thanks!

@marcoregueira
Copy link
Contributor Author

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 2010-01-02-creating-a-substitute.markdown.

image

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 ForPartsOf<ITestInterface, TestClass> will behave exactly as For<> if TestClass doesn't implement the interface. If you feel the check is necessary, I think I will just add it to the implementation of ForPastOf as the first step. It would be more difficult to check this at a later step.

@marcoregueira
Copy link
Contributor Author

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
NSubstitute.Acceptance.Specs\SubbingForConcreteTypesAndMultipleInterfaces.cs
and
NSubstitute.Acceptance.Specs\PartialSubs.cs

Copy link
Member

@dtchepak dtchepak left a 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?

@marcoregueira
Copy link
Contributor Author

marcoregueira commented Oct 15, 2022 via email

@marcoregueira
Copy link
Contributor Author

Hi again

I've added the changes you suggested:

  • Renamed the method to ForTypeForwardingTo and moved all tests, accordingly, to a new class named TypeForwarding. All changes in PartialSubs have been removed.

  • Created new specific exceptions a shown in the following image.

Please, kindly let me know if you find something else to improve.

image

@johncrim
Copy link

I would love to have this integrated in NSubstitute... @marcoregueira - thank you for your work on this!

@NidhalWers
Copy link

Thanks @marcoregueira for your solution, it's something I look forward to have in NSubstitute.
@dtchepak Is there any blocker to integrate this ?

@dtchepak
Copy link
Member

dtchepak commented May 8, 2024

Thanks @marcoregueira for your solution, it's something I look forward to have in NSubstitute. @dtchepak Is there any blocker to integrate this ?

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. ForPartsOf and manually forwarding to the concrete type for the standard NSub features.

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.

@marcoregueira
Copy link
Contributor Author

@dtchepak,

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 TypeForwarding.cs.

Copy link
Member

@dtchepak dtchepak left a 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 👍

@@ -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)
Copy link
Member

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).

Comment on lines 101 to 106
//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);
//}
Copy link
Member

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.

Comment on lines 90 to 100
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);
}

Copy link
Member

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?

Comment on lines 1 to 2


Copy link
Member

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 @@

Copy link
Member

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.
@marcoregueira
Copy link
Contributor Author

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:

  • Updated namespaces and adjusted indentation to align with the new file structure.
  • Fixed changes in the includes section.
  • Simplified collection initializations using collection expressions.
  • Addressed primary constructor warnings.
  • Removed unnecessary comments.
  • Added documentation coments.

Comment on lines +7 to +12
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);
}
Copy link
Member

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);
Copy link
Member

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 ."..

Comment on lines -14 to +13
!castleInvocation.MethodInvocationTarget.IsFinal)
!castleInvocation.MethodInvocationTarget.IsAbstract)
Copy link
Member

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
Copy link
Member

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"

src/NSubstitute/Substitute.cs Show resolved Hide resolved
@dtchepak dtchepak merged commit 4d258a2 into nsubstitute:main Aug 11, 2024
12 checks passed
@dtchepak
Copy link
Member

Thanks @marcoregueira !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants