Skip to content

Commit

Permalink
Consider types assignable to open generics (fluentassertions#954)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonoughe committed Oct 31, 2018
1 parent 7738a65 commit d3da129
Show file tree
Hide file tree
Showing 2 changed files with 183 additions and 3 deletions.
63 changes: 61 additions & 2 deletions Src/FluentAssertions/Types/TypeAssertions.cs
Expand Up @@ -72,6 +72,45 @@ public new AndConstraint<TypeAssertions> BeAssignableTo<T>(string because = "",
return BeAssignableTo(typeof(T), because, becauseArgs);
}

bool CheckIfAssignableToOpenGeneric(Type definition)
{
// The CLR type system does not consider anything to be assignable to an open generic type.
// For the purposes of test assertions, the user probably means that the subject type is
// assignable to any generic type based on the given generic type definition.

if (definition.GetTypeInfo().IsInterface)
{
// check subject and its interfaces against definition
TypeInfo subjectInfo = Subject.GetTypeInfo();
if (subjectInfo.IsInterface && subjectInfo.IsGenericType &&
subjectInfo.GetGenericTypeDefinition().IsSameOrEqualTo(definition))
{
return true;
}
foreach (TypeInfo iface in subjectInfo.ImplementedInterfaces.Select(i => i.GetTypeInfo()))
{
if (iface.IsGenericType && iface.GetGenericTypeDefinition().IsSameOrEqualTo(definition))
{
return true;
}
}
}
else
{
// check subject and its base types against definition
for (TypeInfo baseType = Subject.GetTypeInfo(); baseType != null;
baseType = baseType.BaseType?.GetTypeInfo())
{
if (baseType.IsGenericType && baseType.GetGenericTypeDefinition().IsSameOrEqualTo(definition))
{
return true;
}
}
}

return false;
}

/// <summary>
/// Asserts than an instance of the subject type is assignable variable of given <paramref name="type"/>.
/// </summary>
Expand All @@ -81,8 +120,18 @@ public new AndConstraint<TypeAssertions> BeAssignableTo<T>(string because = "",
/// <returns>An <see cref="AndConstraint{T}"/> which can be used to chain assertions.</returns>
public new AndConstraint<TypeAssertions> BeAssignableTo(Type type, string because = "", params object[] becauseArgs)
{
bool condition;
if (type.GetTypeInfo().IsGenericTypeDefinition)
{
condition = CheckIfAssignableToOpenGeneric(type);
}
else
{
condition = type.IsAssignableFrom(Subject);
}

Execute.Assertion
.ForCondition(type.IsAssignableFrom(Subject))
.ForCondition(condition)
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:" + Identifier + "} {0} to be assignable to {1}{reason}, but it is not.",
Expand Down Expand Up @@ -113,8 +162,18 @@ public new AndConstraint<TypeAssertions> NotBeAssignableTo<T>(string because = "
/// <returns>An <see cref="AndConstraint{T}"/> which can be used to chain assertions.</returns>
public new AndConstraint<TypeAssertions> NotBeAssignableTo(Type type, string because = "", params object[] becauseArgs)
{
bool condition;
if (type.GetTypeInfo().IsGenericTypeDefinition)
{
condition = !CheckIfAssignableToOpenGeneric(type);
}
else
{
condition = !type.IsAssignableFrom(Subject);
}

Execute.Assertion
.ForCondition(!type.IsAssignableFrom(Subject))
.ForCondition(condition)
.BecauseOf(because, becauseArgs)
.FailWith(
"Expected {context:" + Identifier + "} {0} to not be assignable to {1}{reason}, but it is.",
Expand Down
123 changes: 122 additions & 1 deletion Tests/Shared.Specs/TypeAssertionSpecs.cs
Expand Up @@ -484,6 +484,65 @@ public void When_an_unrelated_type_instance_it_fails_with_a_useful_message()
.WithMessage($"*{typeof(DummyImplementingClass)} to be assignable to {typeof(DateTime)}*failure message*");
}

[Fact]
public void When_constructed_of_open_generic_it_succeeds()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange / Act / Assert
//-----------------------------------------------------------------------------------------------------------
typeof(IDummyInterface<IDummyInterface>).Should().BeAssignableTo(typeof(IDummyInterface<>));
}

[Fact]
public void When_implementation_of_open_generic_interface_it_succeeds()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange / Act / Assert
//-----------------------------------------------------------------------------------------------------------
typeof(ClassWithGenericBaseType).Should().BeAssignableTo(typeof(IDummyInterface<>));
}

[Fact]
public void When_derived_of_open_generic_class_it_succeeds()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange / Act / Assert
//-----------------------------------------------------------------------------------------------------------
typeof(ClassWithGenericBaseType).Should().BeAssignableTo(typeof(DummyBaseType<>));
}

[Fact]
public void When_unrelated_to_open_generic_interface_it_fails_with_a_useful_message()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
Type someType = typeof(IDummyInterface);
Action act = () => someType.Should().BeAssignableTo(typeof(IDummyInterface<>), "because we want to test the failure {0}", "message");

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
.WithMessage($"*{typeof(IDummyInterface)} to be assignable to {typeof(IDummyInterface<>)}*failure message*");
}

[Fact]
public void When_unrelated_to_open_generic_type_it_fails_with_a_useful_message()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
Type someType = typeof(ClassWithAttribute);
Action act = () => someType.Should().BeAssignableTo(typeof(DummyBaseType<>), "because we want to test the failure {0}", "message");

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
.WithMessage($"*{typeof(ClassWithAttribute)} to be assignable to {typeof(DummyBaseType<>)}*failure message*");
}

#endregion

#region NotBeAssignableTo
Expand Down Expand Up @@ -596,6 +655,56 @@ public void When_an_unrelated_type_instance_and_asserting_not_assignable_it_succ
typeof(DummyImplementingClass).Should().NotBeAssignableTo(typeof(DateTime));
}

[Fact]
public void When_unrelated_to_open_generic_interface_and_asserting_not_assignable_it_succeeds()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange / Act / Assert
//-----------------------------------------------------------------------------------------------------------
typeof(ClassWithAttribute).Should().NotBeAssignableTo(typeof(IDummyInterface<>));
}

[Fact]
public void When_unrelated_to_open_generic_class_and_asserting_not_assignable_it_succeeds()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange / Act / Assert
//-----------------------------------------------------------------------------------------------------------
typeof(ClassWithAttribute).Should().NotBeAssignableTo(typeof(DummyBaseType<>));
}

[Fact]
public void When_implementation_of_open_generic_interface_and_asserting_not_assignable_it_fails_with_a_useful_message()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
Type someType = typeof(ClassWithGenericBaseType);
Action act = () => someType.Should().NotBeAssignableTo(typeof(IDummyInterface<>), "because we want to test the failure {0}", "message");

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
.WithMessage($"*{typeof(ClassWithGenericBaseType)} to not be assignable to {typeof(IDummyInterface<>)}*failure message*");
}

[Fact]
public void When_derived_from_open_generic_class_and_asserting_not_assignable_it_fails_with_a_useful_message()
{
//-----------------------------------------------------------------------------------------------------------
// Arrange
//-----------------------------------------------------------------------------------------------------------
Type someType = typeof(ClassWithGenericBaseType);
Action act = () => someType.Should().NotBeAssignableTo(typeof(IDummyInterface<>), "because we want to test the failure {0}", "message");

//-----------------------------------------------------------------------------------------------------------
// Act / Assert
//-----------------------------------------------------------------------------------------------------------
act.Should().Throw<XunitException>()
.WithMessage($"*{typeof(ClassWithGenericBaseType)} to not be assignable to {typeof(IDummyInterface<>)}*failure message*");
}

#endregion

#region BeDerivedFrom
Expand Down Expand Up @@ -4508,14 +4617,26 @@ public interface IDummyInterface
{
}

public class ClassThatImplementsInterface : IDummyInterface
public interface IDummyInterface<T>
{
}

public class ClassThatImplementsInterface : IDummyInterface, IDummyInterface<IDummyInterface>
{
}

public class ClassThatDoesNotImplementInterface
{
}

public class DummyBaseType<T> : IDummyInterface<IDummyInterface>
{
}

public class ClassWithGenericBaseType : DummyBaseType<ClassWithGenericBaseType>
{
}

public class ClassWithMembers
{
protected internal ClassWithMembers() { }
Expand Down

0 comments on commit d3da129

Please sign in to comment.