Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to match arguments whose type is generic, when their concrete type is not known (#786) #814

Merged
merged 6 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
86 changes: 81 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,85 @@ 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>>());
mihnea-radulescu marked this conversation as resolved.
Show resolved Hide resolved
}

[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 MySampleClassArgument : IMyArgument<SampleClass> { }
public class MySampleDerivedClassArgument : MySampleClassArgument { }
}
Loading