Skip to content

Commit 73818a6

Browse files
Unable to match arguments whose type is generic, when their concrete type is not known (#786) (#814)
Unable to match arguments whose type is generic, when their concrete type is not known (#786) Co-authored-by: Mihnea Rădulescu <>
1 parent da82bc5 commit 73818a6

File tree

2 files changed

+122
-10
lines changed

2 files changed

+122
-10
lines changed

src/NSubstitute/Core/Extensions.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,30 @@ public static bool IsCompatibleWith(this object? instance, Type type)
2323
return TypeCanBeNull(requiredType);
2424
}
2525

26-
var instanceType = instance.GetType();
26+
var genericTypeDefinition = type.IsGenericType ? type.GetGenericTypeDefinition() : null;
2727

28-
if (instanceType.IsGenericType && type.IsGenericType
29-
&& instanceType.GetGenericTypeDefinition() == type.GetGenericTypeDefinition())
28+
if (genericTypeDefinition is not null)
3029
{
31-
// both are the same generic type. If their GenericTypeArguments match then they are equivalent
32-
return CallSpecification.TypesAreAllEquivalent(instanceType.GenericTypeArguments, type.GenericTypeArguments);
30+
var instanceType = instance.GetType();
31+
var compatibleInstanceTypes = GetCompatibleTypes(instanceType);
32+
33+
foreach (var aCompatibleInstanceType in compatibleInstanceTypes)
34+
{
35+
if (aCompatibleInstanceType.IsGenericType &&
36+
aCompatibleInstanceType.GetGenericTypeDefinition() == genericTypeDefinition)
37+
{
38+
// both are the same generic type. If their GenericTypeArguments match then they are equivalent
39+
return CallSpecification.TypesAreAllEquivalent(
40+
aCompatibleInstanceType.GenericTypeArguments, type.GenericTypeArguments);
41+
}
42+
}
3343
}
3444

3545
return requiredType.IsInstanceOfType(instance);
3646
}
3747

48+
private static IReadOnlyList<Type> GetCompatibleTypes(Type type) => [type, .. type.GetInterfaces()];
49+
3850
/// <summary>
3951
/// Join the <paramref name="strings"/> using <paramref name="separator"/>.
4052
/// </summary>

tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -300,11 +300,11 @@ public void Should_allow_to_check_received_using_properties_from_other_substitut
300300
public void Throw_with_ambiguous_arguments_when_given_an_arg_matcher_and_a_default_arg_value_v1()
301301
{
302302
Assert.Throws<AmbiguousArgumentsException>(() =>
303-
{
304-
_something.Add(0, Arg.Any<int>()).Returns(1);
305-
Assert.Fail("Should not make it here, as it can't work out which arg the matcher refers to." +
306-
"If it does this will throw an AssertionException rather than AmbiguousArgumentsException.");
307-
});
303+
{
304+
_something.Add(0, Arg.Any<int>()).Returns(1);
305+
Assert.Fail("Should not make it here, as it can't work out which arg the matcher refers to." +
306+
"If it does this will throw an AssertionException rather than AmbiguousArgumentsException.");
307+
});
308308
}
309309

310310
[Test]
@@ -740,9 +740,109 @@ public void Supports_custom_argument_matcher_descriptions()
740740
Assert.That(ex.Message, Contains.Substring("24 is not forty two"));
741741
}
742742

743+
[Test]
744+
public void Supports_matching_generic_interface_bound_type_string_with_class_argument()
745+
{
746+
var service = Substitute.For<IMyService>();
747+
var argument = new MyStringArgument();
748+
749+
service.MyMethod(argument);
750+
751+
service.Received().MyMethod(Arg.Any<IMyArgument<string>>());
752+
}
753+
754+
[Test]
755+
public void Supports_matching_generic_interface_bound_type_custom_class_with_class_argument()
756+
{
757+
var service = Substitute.For<IMyService>();
758+
var argument = new MySampleClassArgument();
759+
760+
service.MyMethod(argument);
761+
762+
service.Received().MyMethod(Arg.Any<IMyArgument<SampleClass>>());
763+
}
764+
765+
[Test]
766+
public void Supports_matching_generic_interface_bound_type_custom_class_with_derived_class_argument()
767+
{
768+
var service = Substitute.For<IMyService>();
769+
var argument = new MySampleDerivedClassArgument();
770+
771+
service.MyMethod(argument);
772+
773+
service.Received().MyMethod(Arg.Any<IMyArgument<SampleClass>>());
774+
}
775+
776+
[Test]
777+
public void Supports_matching_custom_class_with_derived_class_argument()
778+
{
779+
var service = Substitute.For<IMyService>();
780+
var argument = new MySampleDerivedClassArgument();
781+
782+
service.MyMethod(argument);
783+
784+
service.Received().MyMethod(Arg.Any<MySampleClassArgument>());
785+
}
786+
787+
[Test]
788+
public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_class_argument()
789+
{
790+
var service = Substitute.For<IMyService>();
791+
var argument = new MyStringArgument();
792+
793+
service.MyMethod(argument);
794+
795+
service.Received().MyMethod(Arg.Any<IMyArgument<Arg.AnyType>>());
796+
}
797+
798+
[Test]
799+
public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_derived_class_argument()
800+
{
801+
var service = Substitute.For<IMyService>();
802+
var argument = new MySampleDerivedClassArgument();
803+
804+
service.MyMethod(argument);
805+
806+
service.Received().MyMethod(Arg.Any<IMyArgument<Arg.AnyType>>());
807+
}
808+
809+
[Test]
810+
public void Does_not_support_matching_ArgAny_of_type_derived_from_base_type_with_string_type_param_to_other_type_derived_from_base_type()
811+
{
812+
var service = Substitute.For<IMyService>();
813+
var argument = new MyOtherStringArgument();
814+
815+
service.MyMethod(argument);
816+
817+
service.DidNotReceive().MyMethod(Arg.Any<MyStringArgument>());
818+
}
819+
820+
[Test]
821+
public void Does_not_support_matching_ArgAny_of_type_derived_from_base_type_with_custom_type_param_to_other_type_derived_from_base_type()
822+
{
823+
var service = Substitute.For<IMyService>();
824+
var argument = new MyOtherSampleClassArgument();
825+
826+
service.MyMethod(argument);
827+
828+
service.DidNotReceive().MyMethod(Arg.Any<MySampleClassArgument>());
829+
}
830+
743831
[SetUp]
744832
public void SetUp()
745833
{
746834
_something = Substitute.For<ISomething>();
747835
}
836+
837+
public interface IMyService
838+
{
839+
void MyMethod<T>(IMyArgument<T> argument);
840+
}
841+
public interface IMyArgument<T> { }
842+
public class SampleClass { }
843+
public class MyStringArgument : IMyArgument<string> { }
844+
public class MyOtherStringArgument : IMyArgument<string> { }
845+
public class MySampleClassArgument : IMyArgument<SampleClass> { }
846+
public class MyOtherSampleClassArgument : IMyArgument<SampleClass> { }
847+
public class MySampleDerivedClassArgument : MySampleClassArgument { }
748848
}

0 commit comments

Comments
 (0)