diff --git a/src/NSubstitute/Core/Extensions.cs b/src/NSubstitute/Core/Extensions.cs index bb3a4880..608d98fa 100644 --- a/src/NSubstitute/Core/Extensions.cs +++ b/src/NSubstitute/Core/Extensions.cs @@ -23,18 +23,30 @@ public static bool IsCompatibleWith(this object? instance, Type type) return TypeCanBeNull(requiredType); } - var instanceType = instance.GetType(); + var genericTypeDefinition = type.IsGenericType ? type.GetGenericTypeDefinition() : null; - if (instanceType.IsGenericType && type.IsGenericType - && instanceType.GetGenericTypeDefinition() == type.GetGenericTypeDefinition()) + if (genericTypeDefinition is not null) { - // both are the same generic type. If their GenericTypeArguments match then they are equivalent - return CallSpecification.TypesAreAllEquivalent(instanceType.GenericTypeArguments, type.GenericTypeArguments); + var instanceType = instance.GetType(); + var compatibleInstanceTypes = GetCompatibleTypes(instanceType); + + foreach (var aCompatibleInstanceType in compatibleInstanceTypes) + { + if (aCompatibleInstanceType.IsGenericType && + aCompatibleInstanceType.GetGenericTypeDefinition() == genericTypeDefinition) + { + // both are the same generic type. If their GenericTypeArguments match then they are equivalent + return CallSpecification.TypesAreAllEquivalent( + aCompatibleInstanceType.GenericTypeArguments, type.GenericTypeArguments); + } + } } return requiredType.IsInstanceOfType(instance); } + private static IReadOnlyList GetCompatibleTypes(Type type) => [type, .. type.GetInterfaces()]; + /// /// Join the using . /// diff --git a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs index 23151337..8b131553 100644 --- a/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs +++ b/tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs @@ -300,11 +300,11 @@ public void Should_allow_to_check_received_using_properties_from_other_substitut public void Throw_with_ambiguous_arguments_when_given_an_arg_matcher_and_a_default_arg_value_v1() { Assert.Throws(() => - { - _something.Add(0, Arg.Any()).Returns(1); - Assert.Fail("Should not make it here, as it can't work out which arg the matcher refers to." + - "If it does this will throw an AssertionException rather than AmbiguousArgumentsException."); - }); + { + _something.Add(0, Arg.Any()).Returns(1); + Assert.Fail("Should not make it here, as it can't work out which arg the matcher refers to." + + "If it does this will throw an AssertionException rather than AmbiguousArgumentsException."); + }); } [Test] @@ -740,9 +740,109 @@ public void Supports_custom_argument_matcher_descriptions() Assert.That(ex.Message, Contains.Substring("24 is not forty two")); } + [Test] + public void Supports_matching_generic_interface_bound_type_string_with_class_argument() + { + var service = Substitute.For(); + var argument = new MyStringArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + + [Test] + public void Supports_matching_generic_interface_bound_type_custom_class_with_class_argument() + { + var service = Substitute.For(); + var argument = new MySampleClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + + [Test] + public void Supports_matching_generic_interface_bound_type_custom_class_with_derived_class_argument() + { + var service = Substitute.For(); + var argument = new MySampleDerivedClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + + [Test] + public void Supports_matching_custom_class_with_derived_class_argument() + { + var service = Substitute.For(); + var argument = new MySampleDerivedClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any()); + } + + [Test] + public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_class_argument() + { + var service = Substitute.For(); + var argument = new MyStringArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + + [Test] + public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_derived_class_argument() + { + var service = Substitute.For(); + var argument = new MySampleDerivedClassArgument(); + + service.MyMethod(argument); + + service.Received().MyMethod(Arg.Any>()); + } + + [Test] + public void Does_not_support_matching_ArgAny_of_type_derived_from_base_type_with_string_type_param_to_other_type_derived_from_base_type() + { + var service = Substitute.For(); + var argument = new MyOtherStringArgument(); + + service.MyMethod(argument); + + service.DidNotReceive().MyMethod(Arg.Any()); + } + + [Test] + public void Does_not_support_matching_ArgAny_of_type_derived_from_base_type_with_custom_type_param_to_other_type_derived_from_base_type() + { + var service = Substitute.For(); + var argument = new MyOtherSampleClassArgument(); + + service.MyMethod(argument); + + service.DidNotReceive().MyMethod(Arg.Any()); + } + [SetUp] public void SetUp() { _something = Substitute.For(); } + + public interface IMyService + { + void MyMethod(IMyArgument argument); + } + public interface IMyArgument { } + public class SampleClass { } + public class MyStringArgument : IMyArgument { } + public class MyOtherStringArgument : IMyArgument { } + public class MySampleClassArgument : IMyArgument { } + public class MyOtherSampleClassArgument : IMyArgument { } + public class MySampleDerivedClassArgument : MySampleClassArgument { } } \ No newline at end of file