From 6407f60cd3b2d8917d0158bb1f8e3b4989d2fc9f Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sat, 7 Sep 2024 19:21:14 +0200 Subject: [PATCH] SAVEPOINT --- Src/FluentAssertions/Formatting/Formatter.cs | 1 + .../Formatting/MethodInfoFormatter.cs | 31 +++++++ .../Formatting/PropertyInfoFormatter.cs | 13 ++- .../Types/MethodBaseAssertions.cs | 2 +- .../Types/PropertyInfoAssertions.cs | 92 +++++++------------ .../Types/PropertyInfoSelectorAssertions.cs | 3 +- Src/FluentAssertions/Types/TypeAssertions.cs | 37 ++++---- .../Specialized/AssemblyAssertionSpecs.cs | 3 +- .../Types/PropertyInfoAssertionSpecs.cs | 12 +-- .../Types/TypeAssertionSpecs.HaveIndexer.cs | 2 +- .../Types/TypeAssertionSpecs.HaveProperty.cs | 15 +++ 11 files changed, 124 insertions(+), 87 deletions(-) create mode 100644 Src/FluentAssertions/Formatting/MethodInfoFormatter.cs diff --git a/Src/FluentAssertions/Formatting/Formatter.cs b/Src/FluentAssertions/Formatting/Formatter.cs index 93ba19030c..4ac3f13bfd 100644 --- a/Src/FluentAssertions/Formatting/Formatter.cs +++ b/Src/FluentAssertions/Formatting/Formatter.cs @@ -26,6 +26,7 @@ public static class Formatter new XElementValueFormatter(), new XAttributeValueFormatter(), new PropertyInfoFormatter(), + new MethodInfoFormatter(), new NullValueFormatter(), new GuidValueFormatter(), new DateTimeOffsetValueFormatter(), diff --git a/Src/FluentAssertions/Formatting/MethodInfoFormatter.cs b/Src/FluentAssertions/Formatting/MethodInfoFormatter.cs new file mode 100644 index 0000000000..743a1a6c76 --- /dev/null +++ b/Src/FluentAssertions/Formatting/MethodInfoFormatter.cs @@ -0,0 +1,31 @@ +using System.Reflection; + +namespace FluentAssertions.Formatting; + +public class MethodInfoFormatter : IValueFormatter +{ + /// + /// Indicates whether the current can handle the specified . + /// + /// The value for which to create a . + /// + /// if the current can handle the specified value; otherwise, . + /// + public bool CanHandle(object value) + { + return value is MethodInfo; + } + + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + var method = (MethodInfo)value; + if (method is null) + { + formattedGraph.AddFragment(""); + } + else + { + formattedGraph.AddFragment($"{method}"); + } + } +} diff --git a/Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs b/Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs index c9746c2901..7ec15ed750 100644 --- a/Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs +++ b/Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs @@ -18,6 +18,17 @@ public bool CanHandle(object value) public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) { - formattedGraph.AddFragment(((PropertyInfo)value).Name); + var property = (PropertyInfo)value; + + if (property is null) + { + formattedGraph.AddFragment(""); + } + else + { + var propTypeName = property.PropertyType.Name; + + formattedGraph.AddFragment($"{propTypeName} {property.DeclaringType}.{property.Name}"); + } } } diff --git a/Src/FluentAssertions/Types/MethodBaseAssertions.cs b/Src/FluentAssertions/Types/MethodBaseAssertions.cs index 7aaac75eb6..bcd9b5d902 100644 --- a/Src/FluentAssertions/Types/MethodBaseAssertions.cs +++ b/Src/FluentAssertions/Types/MethodBaseAssertions.cs @@ -57,7 +57,7 @@ public AndConstraint HaveAccessModifier( .ForCondition(accessModifier == subjectAccessModifier) .BecauseOf(because, becauseArgs) .FailWith( - $"Expected method {Subject!.Name} to be {accessModifier}{{reason}}, but it is {subjectAccessModifier}."); + "Expected method {0} to be {1}{reason}, but it is {2}.", Subject, accessModifier, subjectAccessModifier); } return new AndConstraint((TAssertions)this); diff --git a/Src/FluentAssertions/Types/PropertyInfoAssertions.cs b/Src/FluentAssertions/Types/PropertyInfoAssertions.cs index a1beee41d7..8841609d11 100644 --- a/Src/FluentAssertions/Types/PropertyInfoAssertions.cs +++ b/Src/FluentAssertions/Types/PropertyInfoAssertions.cs @@ -4,6 +4,7 @@ using System.Reflection; using FluentAssertions.Common; using FluentAssertions.Execution; +using FluentAssertions.Formatting; namespace FluentAssertions.Types; @@ -44,7 +45,7 @@ public AndConstraint BeVirtual( assertionChain .ForCondition(Subject.IsVirtual()) .BecauseOf(because, becauseArgs) - .FailWith($"Expected property {GetDescriptionFor(Subject)} to be virtual{{reason}}, but it is not."); + .FailWith("Expected property {0} to be virtual{{reason}}, but it is not.", Subject); } return new AndConstraint(this); @@ -60,7 +61,8 @@ public AndConstraint BeVirtual( /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint NotBeVirtual([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) + public AndConstraint NotBeVirtual([StringSyntax("CompositeFormat")] string because = "", + params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) @@ -72,7 +74,7 @@ public AndConstraint NotBeVirtual([StringSyntax("Composi assertionChain .ForCondition(!Subject.IsVirtual()) .BecauseOf(because, becauseArgs) - .FailWith($"Expected property {GetDescriptionFor(Subject)} not to be virtual{{reason}}, but it is."); + .FailWith("Expected property {0} not to be virtual{{reason}}, but it is.", Subject); } return new AndConstraint(this); @@ -130,21 +132,16 @@ public AndConstraint BeWritable(CSharpAccessModifier acc assertionChain .BecauseOf(because, becauseArgs) .ForCondition(Subject is not null) - .FailWith($"Expected {Identifier} to be {accessModifier}{{reason}}, but {{context:property}} is ."); + .FailWith($"Expected {{context:property}} to be {accessModifier}{{reason}}, but it is .") + .Then + .ForCondition(Subject!.CanWrite) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {0} to have a setter {{reason}}.", Subject); if (assertionChain.Succeeded) { - assertionChain - .ForCondition(Subject!.CanWrite) - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:property} {0} to have a setter{reason}.", - Subject); - - if (assertionChain.Succeeded) - { - Subject!.GetSetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs); - } + assertionChain.ReuseOnce(); + Subject!.GetSetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs); } return new AndConstraint(this); @@ -166,17 +163,11 @@ public AndConstraint NotBeWritable( assertionChain .BecauseOf(because, becauseArgs) .ForCondition(Subject is not null) - .FailWith("Expected property not to have a setter{reason}, but {context:property} is ."); - - if (assertionChain.Succeeded) - { - assertionChain - .ForCondition(!Subject!.CanWrite) - .BecauseOf(because, becauseArgs) - .FailWith( - "Expected {context:property} {0} not to have a setter{reason}.", - Subject); - } + .FailWith("Expected {context:property} not to have a setter{reason}, but it is .") + .Then + .ForCondition(!Subject!.CanWrite) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {0} not to have a setter {{reason}}.", Subject); return new AndConstraint(this); } @@ -191,7 +182,8 @@ public AndConstraint NotBeWritable( /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint BeReadable([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) + public AndConstraint BeReadable([StringSyntax("CompositeFormat")] string because = "", + params object[] becauseArgs) { assertionChain .BecauseOf(because, becauseArgs) @@ -202,7 +194,7 @@ public AndConstraint BeReadable([StringSyntax("Composite { assertionChain.ForCondition(Subject!.CanRead) .BecauseOf(because, becauseArgs) - .FailWith("Expected property " + Subject.Name + " to have a getter{reason}, but it does not."); + .FailWith("Expected property {0}, to have a getter{reason}, but it does not.", Subject); } return new AndConstraint(this); @@ -229,18 +221,15 @@ public AndConstraint BeReadable(CSharpAccessModifier acc assertionChain .BecauseOf(because, becauseArgs) .ForCondition(Subject is not null) - .FailWith($"Expected {Identifier} to be {accessModifier}{{reason}}, but {{context:property}} is ."); + .FailWith($"Expected {{context:property}} to be {accessModifier}{{reason}}, but it is .") + .Then + .ForCondition(Subject!.CanRead) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {0} to have a getter {{reason}}, but it does not.", Subject); if (assertionChain.Succeeded) { - assertionChain.ForCondition(Subject!.CanRead) - .BecauseOf(because, becauseArgs) - .FailWith("Expected property " + Subject.Name + " to have a getter{reason}, but it does not."); - - if (assertionChain.Succeeded) - { - Subject!.GetGetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs); - } + Subject!.GetGetMethod(nonPublic: true).Should().HaveAccessModifier(accessModifier, because, becauseArgs); } return new AndConstraint(this); @@ -270,8 +259,7 @@ public AndConstraint NotBeReadable( .ForCondition(!Subject!.CanRead) .BecauseOf(because, becauseArgs) .FailWith( - "Expected {context:property} {0} not to have a getter{reason}.", - Subject); + "Expected {0} not to have a getter {{reason}}.", Subject); } return new AndConstraint(this); @@ -303,8 +291,8 @@ public AndConstraint Return(Type propertyType, { assertionChain.ForCondition(Subject!.PropertyType == propertyType) .BecauseOf(because, becauseArgs) - .FailWith("Expected Type of property " + Subject.Name + " to be {0}{reason}, but it is {1}.", - propertyType, Subject.PropertyType); + .FailWith("Expected type of property {2} to be {0}{reason}, but it is {1}.", + propertyType, Subject.PropertyType, Subject); } return new AndConstraint(this); @@ -321,7 +309,8 @@ public AndConstraint Return(Type propertyType, /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint Return([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) + public AndConstraint Return([StringSyntax("CompositeFormat")] string because = "", + params object[] becauseArgs) { return Return(typeof(TReturn), because, becauseArgs); } @@ -353,7 +342,7 @@ public AndConstraint NotReturn(Type propertyType, assertionChain .ForCondition(Subject!.PropertyType != propertyType) .BecauseOf(because, becauseArgs) - .FailWith("Expected Type of property " + Subject.Name + " not to be {0}{reason}, but it is.", propertyType); + .FailWith("Expected Type of property {1} not to be {0}{reason}, but it is.", propertyType, Subject); } return new AndConstraint(this); @@ -370,24 +359,13 @@ public AndConstraint NotReturn(Type propertyType, /// /// Zero or more objects to format using the placeholders in . /// - public AndConstraint NotReturn([StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) + public AndConstraint NotReturn([StringSyntax("CompositeFormat")] string because = "", + params object[] becauseArgs) { return NotReturn(typeof(TReturn), because, becauseArgs); } - internal static string GetDescriptionFor(PropertyInfo property) - { - if (property is null) - { - return string.Empty; - } - - var propTypeName = property.PropertyType.Name; - - return $"{propTypeName} {property.DeclaringType}.{property.Name}"; - } - - internal override string SubjectDescription => GetDescriptionFor(Subject); + internal override string SubjectDescription => Formatter.ToString(Subject); /// /// Returns the type of the subject the assertion applies on. diff --git a/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs b/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs index a4aa31bb56..4aca174681 100644 --- a/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs +++ b/Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs @@ -6,6 +6,7 @@ using System.Reflection; using FluentAssertions.Common; using FluentAssertions.Execution; +using FluentAssertions.Formatting; namespace FluentAssertions.Types; @@ -221,7 +222,7 @@ private PropertyInfo[] GetPropertiesWith() private static string GetDescriptionsFor(IEnumerable properties) { - IEnumerable descriptions = properties.Select(property => PropertyInfoAssertions.GetDescriptionFor(property)); + IEnumerable descriptions = properties.Select(property => Formatter.ToString(property)); return string.Join(Environment.NewLine, descriptions); } diff --git a/Src/FluentAssertions/Types/TypeAssertions.cs b/Src/FluentAssertions/Types/TypeAssertions.cs index 0af347192a..3a94065ab1 100644 --- a/Src/FluentAssertions/Types/TypeAssertions.cs +++ b/Src/FluentAssertions/Types/TypeAssertions.cs @@ -269,7 +269,7 @@ public AndWhich BeDecoratedWith( /// Zero or more objects to format using the placeholders in . /// /// is . - public AndWhichConstraint BeDecoratedWith( + public AndWhich BeDecoratedWith( Expression> isMatchingAttributePredicate, [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) where TAttribute : Attribute @@ -287,7 +287,7 @@ public AndWhichConstraint BeDecoratedWith(this, attributes); + return new AndWhich(this, attributes); } /// @@ -300,7 +300,7 @@ public AndWhichConstraint BeDecoratedWith /// Zero or more objects to format using the placeholders in . /// - public AndWhichConstraint BeDecoratedWithOrInherit( + public AndWhich BeDecoratedWithOrInherit( [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) where TAttribute : Attribute { @@ -312,7 +312,7 @@ public AndWhichConstraint BeDecoratedWithOrInherit(this, attributes); + return new AndWhich(this, attributes); } /// @@ -330,7 +330,7 @@ public AndWhichConstraint BeDecoratedWithOrInherit. /// /// is . - public AndWhichConstraint BeDecoratedWithOrInherit( + public AndWhich BeDecoratedWithOrInherit( Expression> isMatchingAttributePredicate, [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) where TAttribute : Attribute @@ -348,7 +348,7 @@ public AndWhichConstraint BeDecoratedWithOrInherit(this, attributes); + return new AndWhich(this, attributes); } /// @@ -889,7 +889,7 @@ public AndConstraint NotBeStatic([StringSyntax("CompositeFormat" /// is . /// is . /// is empty. - public AndWhichConstraint HaveProperty( + public AndWhich HaveProperty( Type propertyType, string name, [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { @@ -907,7 +907,6 @@ public AndWhichConstraint HaveProperty( if (assertionChain.Succeeded) { propertyInfo = Subject.FindPropertyByName(name); - var propertyInfoDescription = PropertyInfoAssertions.GetDescriptionFor(propertyInfo); assertionChain .BecauseOf(because, becauseArgs) @@ -915,10 +914,10 @@ public AndWhichConstraint HaveProperty( .FailWith($"Expected {propertyType.Name} {Subject}.{name} to exist{{reason}}, but it does not.") .Then .ForCondition(propertyInfo.PropertyType == propertyType) - .FailWith($"Expected {propertyInfoDescription} to be of type {propertyType}{{reason}}, but it is not."); + .FailWith($"Expected {0} to be of type {propertyType}{{reason}}, but it is not.", propertyInfo); } - return new AndWhichConstraint(this, propertyInfo); + return new AndWhich(this, propertyInfo); } /// @@ -936,7 +935,7 @@ public AndWhichConstraint HaveProperty( /// /// is . /// is empty. - public AndWhichConstraint HaveProperty( + public AndWhich HaveProperty( string name, [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { return HaveProperty(typeof(TProperty), name, because, becauseArgs); @@ -968,12 +967,11 @@ public AndConstraint NotHaveProperty(string name, if (assertionChain.Succeeded) { PropertyInfo propertyInfo = Subject.FindPropertyByName(name); - var propertyInfoDescription = PropertyInfoAssertions.GetDescriptionFor(propertyInfo); assertionChain .BecauseOf(because, becauseArgs) .ForCondition(propertyInfo is null) - .FailWith($"Expected {propertyInfoDescription} to not exist{{reason}}, but it does."); + .FailWith("Expected {0} to not exist{{reason}}, but it does.", propertyInfo); } return new AndConstraint(this); @@ -1275,18 +1273,20 @@ public AndConstraint NotHaveExplicitMethod( /// /// is . /// is . - public AndWhichConstraint HaveIndexer( + public AndWhich HaveIndexer( Type indexerType, IEnumerable parameterTypes, [StringSyntax("CompositeFormat")] string because = "", params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(indexerType); Guard.ThrowIfArgumentIsNull(parameterTypes); + string parameterString = GetParameterString(parameterTypes); + assertionChain .BecauseOf(because, becauseArgs) .ForCondition(Subject is not null) .FailWith( - $"Expected {indexerType.Name} {{context:type}}[{GetParameterString(parameterTypes)}] to exist{{reason}}" + + $"Expected {indexerType.Name} {{context:type}}[{parameterString}] to exist{{reason}}" + ", but {context:type} is ."); PropertyInfo propertyInfo = null; @@ -1294,20 +1294,19 @@ public AndWhichConstraint HaveIndexer( if (assertionChain.Succeeded) { propertyInfo = Subject.GetIndexerByParameterTypes(parameterTypes); - var propertyInfoDescription = PropertyInfoAssertions.GetDescriptionFor(propertyInfo); assertionChain .BecauseOf(because, becauseArgs) .ForCondition(propertyInfo is not null) .FailWith( - $"Expected {indexerType.Name} {Subject}[{GetParameterString(parameterTypes)}] to exist{{reason}}" + + $"Expected {indexerType.Name} {Subject}[{parameterString}] to exist{{reason}}" + ", but it does not.") .Then .ForCondition(propertyInfo.PropertyType == indexerType) - .FailWith($"Expected {propertyInfoDescription} to be of type {indexerType}{{reason}}, but it is not."); + .FailWith("Expected {0} to be of type {indexerType}{{reason}}, but it is not.", propertyInfo); } - return new AndWhichConstraint(this, propertyInfo); + return new AndWhich(this, propertyInfo); } /// diff --git a/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs index 0bd236d7bc..debf5e2c06 100644 --- a/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Specialized/AssemblyAssertionSpecs.cs @@ -191,7 +191,8 @@ public void Can_continue_assertions_on_the_found_type() .Which.Should().BeDecoratedWith(); // Assert - act.Should().Throw().WithMessage("blah"); + act.Should().Throw() + .WithMessage("Expected*WellKnownClassWithAttribute*decorated*SerializableAttribute*not found."); } [Fact] diff --git a/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs index e4479a345e..9ca9935680 100644 --- a/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Types/PropertyInfoAssertionSpecs.cs @@ -428,7 +428,7 @@ public void When_asserting_a_readwrite_property_is_not_writable_it_fails_with_us action .Should().Throw() .WithMessage( - "Expected propertyInfo ReadWriteProperty not to have a setter because we want to test the error message."); + "Expected ReadWriteProperty not to have a setter because we want to test the error message."); } [Fact] @@ -444,7 +444,7 @@ public void When_asserting_a_writeonly_property_is_not_writable_it_fails_with_us action .Should().Throw() .WithMessage( - "Expected propertyInfo WriteOnlyProperty not to have a setter because we want to test the error message."); + "Expected WriteOnlyProperty not to have a setter because we want to test the error message."); } [Fact] @@ -459,7 +459,7 @@ public void When_subject_is_null_not_be_writable_should_fail() // Assert act.Should().Throw() - .WithMessage("Expected property not to have a setter *failure message*, but propertyInfo is ."); + .WithMessage("Expected propertyInfo not to have a setter *failure message*, but it is ."); } } @@ -587,7 +587,7 @@ public void When_subject_is_null_be_readable_with_accessmodifier_should_fail() // Assert act.Should().Throw() - .WithMessage("Expected property to be Public *failure message*, but propertyInfo is ."); + .WithMessage("Expected propertyInfo to be Public *failure message*, but it is ."); } [Fact] @@ -652,7 +652,7 @@ public void Do_not_the_check_access_modifier_when_the_property_is_not_writable() // Assert action.Should().Throw() - .WithMessage("Expected propertyInfo ReadOnlyProperty to have a setter."); + .WithMessage("Expected ReadOnlyProperty to have a setter."); } [Fact] @@ -667,7 +667,7 @@ public void When_subject_is_null_be_writable_with_accessmodifier_should_fail() // Assert act.Should().Throw() - .WithMessage("Expected property to be Public *failure message*, but propertyInfo is ."); + .WithMessage("Expected propertyInfo to be Public *failure message*, but it is ."); } [Fact] diff --git a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveIndexer.cs b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveIndexer.cs index 9f3bd5022f..bf1b69182a 100644 --- a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveIndexer.cs +++ b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveIndexer.cs @@ -23,7 +23,7 @@ public void When_asserting_a_type_has_an_indexer_which_it_does_then_it_succeeds( type.Should() .HaveIndexer(typeof(string), [typeof(string)]) .Which.Should() - .BeWritable(CSharpAccessModifier.Internal) + .BeWritable(CSharpAccessModifier.Public) .And.BeReadable(CSharpAccessModifier.Private); // Assert diff --git a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveProperty.cs b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveProperty.cs index fe7f4cbb6a..a7d96a9b0f 100644 --- a/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveProperty.cs +++ b/Tests/FluentAssertions.Specs/Types/TypeAssertionSpecs.HaveProperty.cs @@ -30,6 +30,21 @@ public void When_asserting_a_type_has_a_property_which_it_does_then_it_succeeds( act.Should().NotThrow(); } + [Fact] + public void The_name_of_the_property_is_passed_to_the_chained_assertion() + { + // Arrange + var type = typeof(ClassWithMembers); + + // Act + Action act = () => type + .Should().HaveProperty(typeof(string), "PrivateWriteProtectedReadProperty") + .Which.Should().NotBeWritable(); + + // Assert + act.Should().Throw("Expected property PrivateWriteProtectedReadProperty not to have a setter."); + } + [Fact] public void When_asserting_a_type_has_a_property_which_it_does_not_it_fails() {