From d3da129545d83ca71dc8547192386e9a2aa4675f Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Tue, 30 Oct 2018 22:28:31 -0400 Subject: [PATCH] Consider types assignable to open generics (#954) --- Src/FluentAssertions/Types/TypeAssertions.cs | 63 +++++++++- Tests/Shared.Specs/TypeAssertionSpecs.cs | 123 ++++++++++++++++++- 2 files changed, 183 insertions(+), 3 deletions(-) diff --git a/Src/FluentAssertions/Types/TypeAssertions.cs b/Src/FluentAssertions/Types/TypeAssertions.cs index b70c5428c6..d3bf553dae 100644 --- a/Src/FluentAssertions/Types/TypeAssertions.cs +++ b/Src/FluentAssertions/Types/TypeAssertions.cs @@ -72,6 +72,45 @@ public new AndConstraint BeAssignableTo(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; + } + /// /// Asserts than an instance of the subject type is assignable variable of given . /// @@ -81,8 +120,18 @@ public new AndConstraint BeAssignableTo(string because = "", /// An which can be used to chain assertions. public new AndConstraint 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.", @@ -113,8 +162,18 @@ public new AndConstraint NotBeAssignableTo(string because = " /// An which can be used to chain assertions. public new AndConstraint 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.", diff --git a/Tests/Shared.Specs/TypeAssertionSpecs.cs b/Tests/Shared.Specs/TypeAssertionSpecs.cs index ed1c78998e..2d42e9355d 100644 --- a/Tests/Shared.Specs/TypeAssertionSpecs.cs +++ b/Tests/Shared.Specs/TypeAssertionSpecs.cs @@ -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).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() + .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() + .WithMessage($"*{typeof(ClassWithAttribute)} to be assignable to {typeof(DummyBaseType<>)}*failure message*"); + } + #endregion #region NotBeAssignableTo @@ -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() + .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() + .WithMessage($"*{typeof(ClassWithGenericBaseType)} to not be assignable to {typeof(IDummyInterface<>)}*failure message*"); + } + #endregion #region BeDerivedFrom @@ -4508,7 +4617,11 @@ public interface IDummyInterface { } - public class ClassThatImplementsInterface : IDummyInterface + public interface IDummyInterface + { + } + + public class ClassThatImplementsInterface : IDummyInterface, IDummyInterface { } @@ -4516,6 +4629,14 @@ public class ClassThatDoesNotImplementInterface { } + public class DummyBaseType : IDummyInterface + { + } + + public class ClassWithGenericBaseType : DummyBaseType + { + } + public class ClassWithMembers { protected internal ClassWithMembers() { }