Skip to content

Commit

Permalink
Unable to match arguments whose type is generic, when their concrete…
Browse files Browse the repository at this point in the history
… 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 <>
  • Loading branch information
mihnea-radulescu authored Jun 10, 2024
1 parent da82bc5 commit 73818a6
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 10 deletions.
22 changes: 17 additions & 5 deletions src/NSubstitute/Core/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Type> GetCompatibleTypes(Type type) => [type, .. type.GetInterfaces()];

/// <summary>
/// Join the <paramref name="strings"/> using <paramref name="separator"/>.
/// </summary>
Expand Down
110 changes: 105 additions & 5 deletions tests/NSubstitute.Acceptance.Specs/ArgumentMatching.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AmbiguousArgumentsException>(() =>
{
_something.Add(0, Arg.Any<int>()).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<int>()).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]
Expand Down Expand Up @@ -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<IMyService>();
var argument = new MyStringArgument();

service.MyMethod(argument);

service.Received().MyMethod(Arg.Any<IMyArgument<string>>());
}

[Test]
public void Supports_matching_generic_interface_bound_type_custom_class_with_class_argument()
{
var service = Substitute.For<IMyService>();
var argument = new MySampleClassArgument();

service.MyMethod(argument);

service.Received().MyMethod(Arg.Any<IMyArgument<SampleClass>>());
}

[Test]
public void Supports_matching_generic_interface_bound_type_custom_class_with_derived_class_argument()
{
var service = Substitute.For<IMyService>();
var argument = new MySampleDerivedClassArgument();

service.MyMethod(argument);

service.Received().MyMethod(Arg.Any<IMyArgument<SampleClass>>());
}

[Test]
public void Supports_matching_custom_class_with_derived_class_argument()
{
var service = Substitute.For<IMyService>();
var argument = new MySampleDerivedClassArgument();

service.MyMethod(argument);

service.Received().MyMethod(Arg.Any<MySampleClassArgument>());
}

[Test]
public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_class_argument()
{
var service = Substitute.For<IMyService>();
var argument = new MyStringArgument();

service.MyMethod(argument);

service.Received().MyMethod(Arg.Any<IMyArgument<Arg.AnyType>>());
}

[Test]
public void Supports_matching_generic_interface_bound_type_ArgAnyType_with_derived_class_argument()
{
var service = Substitute.For<IMyService>();
var argument = new MySampleDerivedClassArgument();

service.MyMethod(argument);

service.Received().MyMethod(Arg.Any<IMyArgument<Arg.AnyType>>());
}

[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<IMyService>();
var argument = new MyOtherStringArgument();

service.MyMethod(argument);

service.DidNotReceive().MyMethod(Arg.Any<MyStringArgument>());
}

[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<IMyService>();
var argument = new MyOtherSampleClassArgument();

service.MyMethod(argument);

service.DidNotReceive().MyMethod(Arg.Any<MySampleClassArgument>());
}

[SetUp]
public void SetUp()
{
_something = Substitute.For<ISomething>();
}

public interface IMyService
{
void MyMethod<T>(IMyArgument<T> argument);
}
public interface IMyArgument<T> { }
public class SampleClass { }
public class MyStringArgument : IMyArgument<string> { }
public class MyOtherStringArgument : IMyArgument<string> { }
public class MySampleClassArgument : IMyArgument<SampleClass> { }
public class MyOtherSampleClassArgument : IMyArgument<SampleClass> { }
public class MySampleDerivedClassArgument : MySampleClassArgument { }
}

0 comments on commit 73818a6

Please sign in to comment.