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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/NSubstitute/Core/IProxyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace NSubstitute.Core;

public interface IProxyFactory
{
object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments);
object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments);
}
10 changes: 6 additions & 4 deletions src/NSubstitute/Core/SubstituteFactory.cs
Original file line number Diff line number Diff line change
@@ -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?

using System.Reflection;

using NSubstitute.Exceptions;

namespace NSubstitute.Core;
Expand All @@ -14,7 +16,7 @@ public class SubstituteFactory(ISubstituteStateFactory substituteStateFactory, I
/// <returns></returns>
public object Create(Type[] typesToProxy, object?[] constructorArguments)
{
return Create(typesToProxy, constructorArguments, callBaseByDefault: false);
return Create(typesToProxy, constructorArguments, callBaseByDefault: false, isPartial: false);
}

/// <summary>
Expand All @@ -33,10 +35,10 @@ public object CreatePartial(Type[] typesToProxy, object?[] constructorArguments)
throw new CanNotPartiallySubForInterfaceOrDelegateException(primaryProxyType);
}

return Create(typesToProxy, constructorArguments, callBaseByDefault: true);
return Create(typesToProxy, constructorArguments, callBaseByDefault: true, isPartial: true);
}

private object Create(Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault)
private object Create(Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault, bool isPartial)
{
var substituteState = substituteStateFactory.Create(this);
substituteState.CallBaseConfiguration.CallBaseByDefault = callBaseByDefault;
Expand All @@ -46,7 +48,7 @@ private object Create(Type[] typesToProxy, object?[] constructorArguments, bool

var callRouter = callRouterFactory.Create(substituteState, canConfigureBaseCalls);
var additionalTypes = typesToProxy.Where(x => x != primaryProxyType).ToArray();
var proxy = proxyFactory.GenerateProxy(callRouter, primaryProxyType, additionalTypes, constructorArguments);
var proxy = proxyFactory.GenerateProxy(callRouter, primaryProxyType, additionalTypes, isPartial, constructorArguments);
return proxy;
}

Expand Down
27 changes: 27 additions & 0 deletions src/NSubstitute/Exceptions/TypeForwardingException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

namespace NSubstitute.Exceptions

Check failure on line 3 in src/NSubstitute/Exceptions/TypeForwardingException.cs

View workflow job for this annotation

GitHub Actions / format-verify

Convert to file-scoped namespace

Check failure on line 3 in src/NSubstitute/Exceptions/TypeForwardingException.cs

View workflow job for this annotation

GitHub Actions / format-verify

Convert to file-scoped namespace

Check failure on line 3 in src/NSubstitute/Exceptions/TypeForwardingException.cs

View workflow job for this annotation

GitHub Actions / format-verify

Convert to file-scoped namespace
{
public abstract class TypeForwardingException : SubstituteException
{
protected TypeForwardingException(string message) : base(message) { }
}

public sealed class CanNotForwardCallsToClassNotImplementingInterfaceException : TypeForwardingException
{
public CanNotForwardCallsToClassNotImplementingInterfaceException(Type type) : base(DescribeProblem(type)) { }
private static string DescribeProblem(Type type)
{
return string.Format("The provided class '{0}' doesn't implement all requested interfaces. ", type.Name);
}
}

public sealed class CanNotForwardCallsToAbstractClassException : TypeForwardingException
{
public CanNotForwardCallsToAbstractClassException(Type type) : base(DescribeProblem(type)) { }
private static string DescribeProblem(Type type)
{
return string.Format("The provided class '{0}' is abstract. ", type.Name);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@

using System.Collections.Generic;
using System.Reflection;

using Castle.DynamicProxy;

using NSubstitute.Core;
using NSubstitute.Exceptions;

Expand All @@ -10,14 +14,14 @@ public class CastleDynamicProxyFactory(ICallFactory callFactory, IArgumentSpecif
private readonly ProxyGenerator _proxyGenerator = new ProxyGenerator();
private readonly AllMethodsExceptCallRouterCallsHook _allMethodsExceptCallRouterCallsHook = new AllMethodsExceptCallRouterCallsHook();

public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
{
return typeToProxy.IsDelegate()
? GenerateDelegateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments)
: GenerateTypeProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
: GenerateTypeProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
}

private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)
private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
{
VerifyClassHasNotBeenPassedAsAnAdditionalInterface(additionalInterfaces);

Expand All @@ -31,7 +35,8 @@ private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[
additionalInterfaces,
constructorArguments,
[proxyIdInterceptor, forwardingInterceptor],
proxyGenerationOptions);
proxyGenerationOptions,
isPartial);

forwardingInterceptor.SwitchToFullDispatchMode();
return proxy;
Expand All @@ -54,7 +59,8 @@ private object GenerateDelegateProxy(ICallRouter callRouter, Type delegateType,
additionalInterfaces: null,
constructorArguments: null,
interceptors: [proxyIdInterceptor, forwardingInterceptor],
proxyGenerationOptions);
proxyGenerationOptions,
isPartial: false);

forwardingInterceptor.SwitchToFullDispatchMode();

Expand All @@ -75,8 +81,13 @@ private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter call
private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? additionalInterfaces,
object?[]? constructorArguments,
IInterceptor[] interceptors,
ProxyGenerationOptions proxyGenerationOptions)
ProxyGenerationOptions proxyGenerationOptions,
bool isPartial)
{
if (isPartial)
return CreatePartialProxy(typeToProxy, additionalInterfaces, constructorArguments, interceptors, proxyGenerationOptions, isPartial);


if (typeToProxy.GetTypeInfo().IsInterface)
{
VerifyNoConstructorArgumentsGivenForInterface(constructorArguments);
Expand All @@ -96,11 +107,38 @@ private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter call
additionalInterfaces = interfaces;
}


return _proxyGenerator.CreateClassProxy(typeToProxy,
additionalInterfaces,
proxyGenerationOptions,
constructorArguments,
interceptors);
}

private object CreatePartialProxy(Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, bool isPartial)
{
if (typeToProxy.GetTypeInfo().IsClass &&
additionalInterfaces != null &&
additionalInterfaces.Any())
{
VerifyClassIsNotAbstract(typeToProxy);
VerifyClassImplementsAllInterfaces(typeToProxy, additionalInterfaces);

var targetObject = Activator.CreateInstance(typeToProxy, constructorArguments);
typeToProxy = additionalInterfaces.First();

return _proxyGenerator.CreateInterfaceProxyWithTarget(typeToProxy,
additionalInterfaces,
target: targetObject,
options: proxyGenerationOptions,
interceptors: interceptors);
}

return _proxyGenerator.CreateClassProxy(typeToProxy,
additionalInterfaces,
proxyGenerationOptions,
constructorArguments,
interceptors);
additionalInterfaces,
proxyGenerationOptions,
constructorArguments,
interceptors);
}

private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter callRouter)
Expand All @@ -114,6 +152,22 @@ private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter c
options.AddMixinInstance(new StaticCallRouterProvider(callRouter));

return options;
}

private static void VerifyClassImplementsAllInterfaces(Type classType, IEnumerable<Type> additionalInterfaces)
{
if (!additionalInterfaces.All(x => x.GetTypeInfo().IsAssignableFrom(classType.GetTypeInfo())))
{
throw new CanNotForwardCallsToClassNotImplementingInterfaceException(classType);
}
}

private static void VerifyClassIsNotAbstract(Type classType)
{
if (classType.GetTypeInfo().IsAbstract)
{
throw new CanNotForwardCallsToAbstractClassException(classType);
}
}

private static void VerifyNoConstructorArgumentsGivenForInterface(object?[]? constructorArguments)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

using Castle.DynamicProxy;
using NSubstitute.Core;

Expand All @@ -10,8 +11,7 @@ public virtual ICall Map(IInvocation castleInvocation)
Func<object>? baseMethod = null;
if (castleInvocation.InvocationTarget != null &&
castleInvocation.MethodInvocationTarget.IsVirtual &&
!castleInvocation.MethodInvocationTarget.IsAbstract &&
!castleInvocation.MethodInvocationTarget.IsFinal)
!castleInvocation.MethodInvocationTarget.IsAbstract)
{
baseMethod = CreateBaseResultInvocation(castleInvocation);
}
Expand Down
5 changes: 3 additions & 2 deletions src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

using NSubstitute.Core;
using NSubstitute.Proxies.CastleDynamicProxy;

Expand All @@ -8,9 +9,9 @@ public class DelegateProxyFactory(CastleDynamicProxyFactory objectProxyFactory)
{
private readonly CastleDynamicProxyFactory _castleObjectProxyFactory = objectProxyFactory ?? throw new ArgumentNullException(nameof(objectProxyFactory));

public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
{
// Castle factory can now resolve delegate proxies as well.
return _castleObjectProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
return _castleObjectProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
}
}
9 changes: 6 additions & 3 deletions src/NSubstitute/Proxies/ProxyFactory.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@


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

using NSubstitute.Core;

namespace NSubstitute.Proxies;

[Obsolete("This class is deprecated and will be removed in future versions of the product.")]
public class ProxyFactory(IProxyFactory delegateFactory, IProxyFactory dynamicProxyFactory) : IProxyFactory
{
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)

public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
{
var isDelegate = typeToProxy.IsDelegate();
return isDelegate
? delegateFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments)
: dynamicProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
? delegateFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments)
: dynamicProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
}
}
16 changes: 16 additions & 0 deletions src/NSubstitute/Substitute.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

using NSubstitute.Core;

// Disable nullability for client API, so it does not affect clients.
Expand Down Expand Up @@ -89,4 +90,19 @@
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).

where T : class
{
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
return (T)substituteFactory.CreatePartial(new[] { typeof(T), typeof(TClass) }, constructorArguments);

Check failure on line 98 in src/NSubstitute/Substitute.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

Check failure on line 98 in src/NSubstitute/Substitute.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

Check failure on line 98 in src/NSubstitute/Substitute.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified
}

Comment on lines 90 to +100
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?

//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);
//}
Comment on lines +101 to +106
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.


}
3 changes: 2 additions & 1 deletion tests/NSubstitute.Acceptance.Specs/PartialSubs.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NSubstitute.Core;

using NSubstitute.Core;
using NSubstitute.Exceptions;
using NSubstitute.Extensions;
using NUnit.Framework;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@


using NUnit.Framework;
using NUnit.Framework.Legacy;

namespace NSubstitute.Acceptance.Specs;

Expand Down Expand Up @@ -31,6 +34,30 @@
subAsIFirst.Received().First();
}

[Test]
public void Can_sub_for_abstract_type_and_implement_other_two_interfaces()
{
// test from docs
var substitute = Substitute.For(new[] { typeof(IFirst), typeof(ISecond), typeof(ClassWithCtorArgs) },

Check warning on line 41 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

Check warning on line 41 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified
new object[] { "hello world", 5 });

Check warning on line 42 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

Check warning on line 42 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

ClassicAssert.IsInstanceOf<IFirst>(substitute);
ClassicAssert.IsInstanceOf<ISecond>(substitute);
ClassicAssert.IsInstanceOf<ClassWithCtorArgs>(substitute);
}

[Test]
public void Can_sub_for_concrete_type_and_implement_other_two_interfaces()
{
// test from docs
var substitute = Substitute.For(new[] { typeof(IFirst), typeof(ISecond), typeof(ConcreteClassWithCtorArgs) },

Check warning on line 53 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

Check warning on line 53 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified
new object[] { "hello world", 5 });

Check warning on line 54 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

Check warning on line 54 in tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

View workflow job for this annotation

GitHub Actions / format-verify

Collection initialization can be simplified

ClassicAssert.IsInstanceOf<IFirst>(substitute);
ClassicAssert.IsInstanceOf<ISecond>(substitute);
ClassicAssert.IsInstanceOf<ConcreteClassWithCtorArgs>(substitute);
}

[Test]
public void Partial_sub()
{
Expand Down Expand Up @@ -94,4 +121,10 @@
{
public string StringFromCtorArg { get; set; } = s; public int IntFromCtorArg { get; set; } = a;
}
public class ConcreteClassWithCtorArgs : ClassWithCtorArgs
{
public ConcreteClassWithCtorArgs(string s, int a) : base(s, a)
{
}
}
}