Skip to content

Commit

Permalink
Fix: Mocking a class and two unrelated interfaces didn't work
Browse files Browse the repository at this point in the history
The case were you can create a mixin with a class and two unrelated interfaces was show in the docs but not tested in code.

This commit adds a new test and a fix for this use case.

As a collateral, the new ForPartsOf<ITestInterface, TestNonVirtualClass> will not check if the class implements the interface. In such a case it will behave just like For<>;
  • Loading branch information
marcoregueira committed Jul 21, 2022
1 parent 1e1f4e2 commit 302440e
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

Expand Down Expand Up @@ -87,9 +88,9 @@ private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter call
}

private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? additionalInterfaces,
object?[]? constructorArguments,
IInterceptor[] interceptors,
ProxyGenerationOptions proxyGenerationOptions)
object?[]? constructorArguments,
IInterceptor[] interceptors,
ProxyGenerationOptions proxyGenerationOptions)
{
additionalInterfaces ??= new Type[] { };
var classToInstantiate = additionalInterfaces
Expand All @@ -100,39 +101,49 @@ private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter call
.Where(x => x.GetTypeInfo().IsInterface)
.ToList();


if (classToInstantiate != null &&
typeToProxy.GetTypeInfo().IsInterface &&
typeToProxy.IsAssignableFrom(classToInstantiate))
{
return CreateProxyForClassImplementingInterface(
typeToProxy,
constructorArguments,
interceptors,
proxyGenerationOptions,
classToInstantiate,
implementedInterfaces);
}

return CreateMixin(
typeToProxy,
additionalInterfaces,
constructorArguments,
interceptors,
proxyGenerationOptions,
classToInstantiate);
}

private object CreateMixin(Type typeToProxy, Type[] additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, Type? classToInstantiate)
{
if (typeToProxy.GetTypeInfo().IsInterface)
{
if (classToInstantiate != null)
{
VerifyClassImplementsInterface(classToInstantiate, typeToProxy);
VerifyClassIsNotAbstract(classToInstantiate);
var targetObject = Activator.CreateInstance(classToInstantiate, constructorArguments);
implementedInterfaces.Add(typeToProxy);

return _proxyGenerator.CreateInterfaceProxyWithTarget(typeToProxy,
implementedInterfaces.ToArray(),
target: targetObject,
options: proxyGenerationOptions,
interceptors: interceptors);
}
else
{
if (classToInstantiate == null)
VerifyNoConstructorArgumentsGivenForInterface(constructorArguments);

var interfacesArrayLength = additionalInterfaces != null ? additionalInterfaces.Length + 1 : 1;
var interfaces = new Type[interfacesArrayLength];

interfaces[0] = typeToProxy;
if (additionalInterfaces != null)
{
Array.Copy(additionalInterfaces, 0, interfaces, 1, additionalInterfaces.Length);
}
var interfacesArrayLength = additionalInterfaces != null ? additionalInterfaces.Length + 1 : 1;
var interfaces = new Type[interfacesArrayLength];

// We need to create a proxy for the object type, so we can intercept the ToString() method.
// Therefore, we put the desired primary interface to the secondary list.
typeToProxy = typeof(object);
additionalInterfaces = interfaces;
interfaces[0] = typeToProxy;
if (additionalInterfaces != null)
{
Array.Copy(additionalInterfaces, 0, interfaces, 1, additionalInterfaces.Length);
}

// We need to create a proxy for the object type, so we can intercept the ToString() method.
// Therefore, we put the desired primary interface to the secondary list.
typeToProxy = classToInstantiate ?? typeof(object);
additionalInterfaces = interfaces;
}

return _proxyGenerator.CreateClassProxy(typeToProxy,
Expand All @@ -142,6 +153,18 @@ private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter call
interceptors);
}

private object CreateProxyForClassImplementingInterface(Type typeToProxy, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, Type classToInstantiate, List<Type> implementedInterfaces)
{
VerifyClassIsNotAbstract(classToInstantiate);
var targetObject = Activator.CreateInstance(classToInstantiate, constructorArguments);
implementedInterfaces.Add(typeToProxy);

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

private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter callRouter)
{
Expand Down
16 changes: 4 additions & 12 deletions tests/NSubstitute.Acceptance.Specs/PartialSubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,6 @@ public void PartialSubstituteCallsConstructorWithParameters()
Assert.That(testInterface.CalledTimes, Is.EqualTo(51));
}

[Test]
public void PartialSubstituteFailsIfClassDoesntImplementInterface()
{
Assert.Throws<SubstituteException>(
() => Substitute.ForPartsOf<ITestInterface, TestAbstractClass>());
}


[Test]
public void PartialSubstituteFailsIfClassIsAbstract()
{
Expand Down Expand Up @@ -269,7 +261,7 @@ public void CallBaseForNonPartialProxyWhenExplicitlyEnabled()
substitute.When(x => x.MethodReturnsSameInt(Arg.Any<int>())).CallBase();

substitute.MethodReturnsSameInt(42);

Assert.That(substitute.CalledTimes, Is.EqualTo(1));
}

Expand All @@ -281,7 +273,7 @@ public void CallBaseWhenDisabledViaRouterButExplicitlyEnabled()
substitute.When(x => x.MethodReturnsSameInt(Arg.Any<int>())).CallBase();

substitute.MethodReturnsSameInt(42);

Assert.That(substitute.CalledTimes, Is.EqualTo(1));
}

Expand Down Expand Up @@ -310,7 +302,7 @@ public void CallBaseWhenExplicitlyDisabledAndThenEnabled()

Assert.That(substitute.CalledTimes, Is.EqualTo(1));
}

[Test]
public void ShouldThrowExceptionIfTryToNotCallBaseForAbstractMethod()
{
Expand All @@ -330,7 +322,7 @@ public void ShouldThrowExceptionIfTryToNotCallBaseForInterfaceProxy()
() => substitute.When(x => x.TestMethodReturnsInt()).DoNotCallBase());
Assert.That(ex.Message, Contains.Substring("base method implementation is missing"));
}

[Test]
public void ShouldThrowExceptionIfTryToCallBaseForAbstractMethod()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ public void Can_sub_for_concrete_type_and_implement_other_interfaces()
subAsIFirst.Received().First();
}

[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(ClassWithCtorArgs) },
new object[] { "hello world", 5 });

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

[Test]
public void Partial_sub()
{
Expand Down

0 comments on commit 302440e

Please sign in to comment.