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

Consider types assignable to open generics (#954) #955

Merged
merged 7 commits into from Nov 8, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
mdonoughe marked this conversation as resolved.
Show resolved Hide resolved
{
// 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙃 I personally find all these intermediate return statements a more difficult to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this better now?

}
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;
mdonoughe marked this conversation as resolved.
Show resolved Hide resolved
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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙃 We normally use the When_...._it_should_.... format for all our tests.
🤔 I don't get the when constructed. We're not constructing anything here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Microsoft uses the term "constructed" when referring to a generic type that has its arguments filled in: https://docs.microsoft.com/en-us/dotnet/api/System.Type.IsConstructedGenericType?view=netframework-4.7.2 . I hadn't seen it before either.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we're missing some kind of context for a group of specs so that we don't have to repeat the cause and effect.

{
//-----------------------------------------------------------------------------------------------------------
// Arrange / Act / Assert
//-----------------------------------------------------------------------------------------------------------
typeof(IDummyInterface<IDummyInterface>).Should().BeAssignableTo(typeof(IDummyInterface<>));
}

[Fact]
public void When_implementation_of_open_generic_interface_it_succeeds()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙃 It feels like the name is missing a verb or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to emulate the "When_its_own_type_instance_it_succeeds" pattern the other BeAssignableTo methods were following without making the names super long.

{
//-----------------------------------------------------------------------------------------------------------
// 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