Skip to content

Commit 302440e

Browse files
committed
Fix: Mocking a class and two unrelated interfaces didn't work
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<>;
1 parent 1e1f4e2 commit 302440e

File tree

3 files changed

+69
-42
lines changed

3 files changed

+69
-42
lines changed

src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Reflection;
45

@@ -87,9 +88,9 @@ private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter call
8788
}
8889

8990
private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? additionalInterfaces,
90-
object?[]? constructorArguments,
91-
IInterceptor[] interceptors,
92-
ProxyGenerationOptions proxyGenerationOptions)
91+
object?[]? constructorArguments,
92+
IInterceptor[] interceptors,
93+
ProxyGenerationOptions proxyGenerationOptions)
9394
{
9495
additionalInterfaces ??= new Type[] { };
9596
var classToInstantiate = additionalInterfaces
@@ -100,39 +101,49 @@ private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? ad
100101
.Where(x => x.GetTypeInfo().IsInterface)
101102
.ToList();
102103

104+
105+
if (classToInstantiate != null &&
106+
typeToProxy.GetTypeInfo().IsInterface &&
107+
typeToProxy.IsAssignableFrom(classToInstantiate))
108+
{
109+
return CreateProxyForClassImplementingInterface(
110+
typeToProxy,
111+
constructorArguments,
112+
interceptors,
113+
proxyGenerationOptions,
114+
classToInstantiate,
115+
implementedInterfaces);
116+
}
117+
118+
return CreateMixin(
119+
typeToProxy,
120+
additionalInterfaces,
121+
constructorArguments,
122+
interceptors,
123+
proxyGenerationOptions,
124+
classToInstantiate);
125+
}
126+
127+
private object CreateMixin(Type typeToProxy, Type[] additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, Type? classToInstantiate)
128+
{
103129
if (typeToProxy.GetTypeInfo().IsInterface)
104130
{
105-
if (classToInstantiate != null)
106-
{
107-
VerifyClassImplementsInterface(classToInstantiate, typeToProxy);
108-
VerifyClassIsNotAbstract(classToInstantiate);
109-
var targetObject = Activator.CreateInstance(classToInstantiate, constructorArguments);
110-
implementedInterfaces.Add(typeToProxy);
111-
112-
return _proxyGenerator.CreateInterfaceProxyWithTarget(typeToProxy,
113-
implementedInterfaces.ToArray(),
114-
target: targetObject,
115-
options: proxyGenerationOptions,
116-
interceptors: interceptors);
117-
}
118-
else
119-
{
131+
if (classToInstantiate == null)
120132
VerifyNoConstructorArgumentsGivenForInterface(constructorArguments);
121133

122-
var interfacesArrayLength = additionalInterfaces != null ? additionalInterfaces.Length + 1 : 1;
123-
var interfaces = new Type[interfacesArrayLength];
124-
125-
interfaces[0] = typeToProxy;
126-
if (additionalInterfaces != null)
127-
{
128-
Array.Copy(additionalInterfaces, 0, interfaces, 1, additionalInterfaces.Length);
129-
}
134+
var interfacesArrayLength = additionalInterfaces != null ? additionalInterfaces.Length + 1 : 1;
135+
var interfaces = new Type[interfacesArrayLength];
130136

131-
// We need to create a proxy for the object type, so we can intercept the ToString() method.
132-
// Therefore, we put the desired primary interface to the secondary list.
133-
typeToProxy = typeof(object);
134-
additionalInterfaces = interfaces;
137+
interfaces[0] = typeToProxy;
138+
if (additionalInterfaces != null)
139+
{
140+
Array.Copy(additionalInterfaces, 0, interfaces, 1, additionalInterfaces.Length);
135141
}
142+
143+
// We need to create a proxy for the object type, so we can intercept the ToString() method.
144+
// Therefore, we put the desired primary interface to the secondary list.
145+
typeToProxy = classToInstantiate ?? typeof(object);
146+
additionalInterfaces = interfaces;
136147
}
137148

138149
return _proxyGenerator.CreateClassProxy(typeToProxy,
@@ -142,6 +153,18 @@ private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? ad
142153
interceptors);
143154
}
144155

156+
private object CreateProxyForClassImplementingInterface(Type typeToProxy, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, Type classToInstantiate, List<Type> implementedInterfaces)
157+
{
158+
VerifyClassIsNotAbstract(classToInstantiate);
159+
var targetObject = Activator.CreateInstance(classToInstantiate, constructorArguments);
160+
implementedInterfaces.Add(typeToProxy);
161+
162+
return _proxyGenerator.CreateInterfaceProxyWithTarget(typeToProxy,
163+
implementedInterfaces.ToArray(),
164+
target: targetObject,
165+
options: proxyGenerationOptions,
166+
interceptors: interceptors);
167+
}
145168

146169
private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter callRouter)
147170
{

tests/NSubstitute.Acceptance.Specs/PartialSubs.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,6 @@ public void PartialSubstituteCallsConstructorWithParameters()
131131
Assert.That(testInterface.CalledTimes, Is.EqualTo(51));
132132
}
133133

134-
[Test]
135-
public void PartialSubstituteFailsIfClassDoesntImplementInterface()
136-
{
137-
Assert.Throws<SubstituteException>(
138-
() => Substitute.ForPartsOf<ITestInterface, TestAbstractClass>());
139-
}
140-
141-
142134
[Test]
143135
public void PartialSubstituteFailsIfClassIsAbstract()
144136
{
@@ -269,7 +261,7 @@ public void CallBaseForNonPartialProxyWhenExplicitlyEnabled()
269261
substitute.When(x => x.MethodReturnsSameInt(Arg.Any<int>())).CallBase();
270262

271263
substitute.MethodReturnsSameInt(42);
272-
264+
273265
Assert.That(substitute.CalledTimes, Is.EqualTo(1));
274266
}
275267

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

283275
substitute.MethodReturnsSameInt(42);
284-
276+
285277
Assert.That(substitute.CalledTimes, Is.EqualTo(1));
286278
}
287279

@@ -310,7 +302,7 @@ public void CallBaseWhenExplicitlyDisabledAndThenEnabled()
310302

311303
Assert.That(substitute.CalledTimes, Is.EqualTo(1));
312304
}
313-
305+
314306
[Test]
315307
public void ShouldThrowExceptionIfTryToNotCallBaseForAbstractMethod()
316308
{
@@ -330,7 +322,7 @@ public void ShouldThrowExceptionIfTryToNotCallBaseForInterfaceProxy()
330322
() => substitute.When(x => x.TestMethodReturnsInt()).DoNotCallBase());
331323
Assert.That(ex.Message, Contains.Substring("base method implementation is missing"));
332324
}
333-
325+
334326
[Test]
335327
public void ShouldThrowExceptionIfTryToCallBaseForAbstractMethod()
336328
{

tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ public void Can_sub_for_concrete_type_and_implement_other_interfaces()
3232
subAsIFirst.Received().First();
3333
}
3434

35+
[Test]
36+
public void Can_sub_for_concrete_type_and_implement_other_two_interfaces()
37+
{
38+
// test from docs
39+
var substitute = Substitute.For(new[] { typeof(IFirst), typeof(ISecond), typeof(ClassWithCtorArgs) },
40+
new object[] { "hello world", 5 });
41+
42+
Assert.IsInstanceOf<IFirst>(substitute);
43+
Assert.IsInstanceOf<ISecond>(substitute);
44+
Assert.IsInstanceOf<ClassWithCtorArgs>(substitute);
45+
}
46+
3547
[Test]
3648
public void Partial_sub()
3749
{

0 commit comments

Comments
 (0)