diff --git a/Src/FluentAssertions/Collections/CollectionAssertions.cs b/Src/FluentAssertions/Collections/CollectionAssertions.cs index b795e0a79d..920b9980a5 100644 --- a/Src/FluentAssertions/Collections/CollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/CollectionAssertions.cs @@ -349,6 +349,71 @@ public AndConstraint BeEquivalentTo(params object[] expectations) return new AndConstraint((TAssertions)this); } + /// + /// Asserts that a collection of objects is equivalent to another collection of objects. + /// + /// + /// Objects within the collections are equivalent when both object graphs have equally named properties with the same + /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another + /// and the result is equal. + /// The type of a collection property is ignored as long as the collection implements and all + /// items in the collection are structurally equal. + /// Notice that actual behavior is determined by the global defaults managed by . + /// + public AndConstraint BeEquivalentTo(IEnumerable expectation, string because = "", params object[] becauseArgs) + { + BeEquivalentTo(expectation, config => config, because, becauseArgs); + + return new AndConstraint((TAssertions)this); + } + + /// + /// Asserts that a collection of objects is equivalent to another collection of objects. + /// + /// + /// Objects within the collections are equivalent when both object graphs have equally named properties with the same + /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another + /// and the result is equal. + /// The type of a collection property is ignored as long as the collection implements and all + /// items in the collection are structurally equal. + /// Notice that actual behavior is determined by the global defaults managed by . + /// + /// + /// A reference to the configuration object that can be used + /// to influence the way the object graphs are compared. You can also provide an alternative instance of the + /// class. The global defaults are determined by the + /// class. + /// + /// + /// An optional formatted phrase as is supported by explaining why the + /// assertion is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeEquivalentTo(IEnumerable expectation, + Func, EquivalencyAssertionOptions> config, string because = "", + params object[] becauseArgs) + { + EquivalencyAssertionOptions options = config(AssertionOptions.CloneDefaults()); + + var context = new EquivalencyValidationContext + { + Subject = Subject, + Expectation = expectation, + RootIsCollection = true, + CompileTimeType = typeof(IEnumerable), + Because = because, + BecauseArgs = becauseArgs, + Tracer = options.TraceWriter + }; + + var equivalencyValidator = new EquivalencyValidator(options); + equivalencyValidator.AssertEquality(context); + + return new AndConstraint((TAssertions)this); + } + /// /// Asserts that a collection of objects is equivalent to another collection of objects. /// @@ -1371,7 +1436,9 @@ public AndConstraint HaveElementPreceding(object successor, object .Then .Given(subject => PredecessorOf(successor, subject)) .ForCondition(predecessor => predecessor.IsSameOrEqualTo(expectation)) - .FailWith("but found {0}.", predecessor => predecessor); + .FailWith("but found {0}.", predecessor => predecessor) + .Then + .ClearExpectation(); return new AndConstraint((TAssertions)this); } diff --git a/Src/FluentAssertions/Collections/NonGenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/NonGenericCollectionAssertions.cs index 89cb6d125d..7f462141d6 100644 --- a/Src/FluentAssertions/Collections/NonGenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/NonGenericCollectionAssertions.cs @@ -301,72 +301,5 @@ private int GetMostLocalCount() return base.NotContain(new[] { unexpected }, because, becauseArgs); } - - /// - /// Asserts that a collection of objects is equivalent to another collection of objects. - /// - /// - /// Objects within the collections are equivalent when both object graphs have equally named properties with the same - /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another - /// and the result is equal. - /// The type of a collection property is ignored as long as the collection implements and all - /// items in the collection are structurally equal. - /// Notice that actual behavior is determined by the global defaults managed by . - /// - /// - /// An optional formatted phrase as is supported by explaining why the - /// assertion is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public void BeEquivalentTo(IEnumerable expectation, string because = "", params object[] becauseArgs) - { - BeEquivalentTo(expectation, config => config, because, becauseArgs); - } - - /// - /// Asserts that a collection of objects is equivalent to another collection of objects. - /// - /// - /// Objects within the collections are equivalent when both object graphs have equally named properties with the same - /// value, irrespective of the type of those objects. Two properties are also equal if one type can be converted to another - /// and the result is equal. - /// The type of a collection property is ignored as long as the collection implements and all - /// items in the collection are structurally equal. - /// - /// - /// A reference to the configuration object that can be used - /// to influence the way the object graphs are compared. You can also provide an alternative instance of the - /// class. The global defaults are determined by the - /// class. - /// - /// - /// An optional formatted phrase as is supported by explaining why the - /// assertion is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public void BeEquivalentTo(IEnumerable expectation, - Func, EquivalencyAssertionOptions> config, string because = "", - params object[] becauseArgs) - { - EquivalencyAssertionOptions options = config(AssertionOptions.CloneDefaults()); - - var context = new EquivalencyValidationContext - { - Subject = Subject, - Expectation = expectation, - RootIsCollection = true, - CompileTimeType = typeof(IEnumerable), - Because = because, - BecauseArgs = becauseArgs, - Tracer = options.TraceWriter - }; - - var equivalencyValidator = new EquivalencyValidator(options); - equivalencyValidator.AssertEquality(context); - } } } diff --git a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs index 880b5141b1..c27a63ae0a 100644 --- a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs +++ b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidator.cs @@ -31,7 +31,7 @@ public EnumerableEquivalencyValidator(IEquivalencyValidator parent, IEquivalency public void Execute(object[] subject, T[] expectation) { - if (AssertIsNotNull(expectation, subject) && EnumerableEquivalencyValidatorExtensions.AssertCollectionsHaveSameCount(subject, expectation)) + if (AssertIsNotNull(expectation, subject) && AssertCollectionsHaveSameCount(subject, expectation)) { if (Recursive) { @@ -57,6 +57,19 @@ private bool AssertIsNotNull(object expectation, object[] subject) .FailWith("Expected {context:subject} to be , but found {0}.", new object[] { subject }); } + private static Continuation AssertCollectionsHaveSameCount(ICollection subject, ICollection expectation) + { + return AssertionScope.Current + .WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count) + .AssertEitherCollectionIsNotEmpty(subject, expectation) + .Then + .AssertCollectionHasEnoughItems(subject, expectation) + .Then + .AssertCollectionHasNotTooManyItems(subject, expectation) + .Then + .ClearExpectation(); + } + private void AssertElementGraphEquivalency(object[] subjects, T[] expectations) { unmatchedSubjectIndexes = new List(subjects.Length); @@ -90,7 +103,6 @@ private void LooselyMatchAgainst(IList subjects, T expectation, int e var results = new AssertionResultSet(); int index = 0; GetTraceMessage getMessage = path => $"Comparing subject at {path}[{index}] with the expectation at {path}[{expectationIndex}]"; - int count = subjects.Count; int indexToBeRemoved = -1; for (var metaIndex = 0; metaIndex < unmatchedSubjectIndexes.Count; metaIndex++) diff --git a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidatorExtensions.cs b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidatorExtensions.cs index 32129eefc0..b02a1a10a9 100644 --- a/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidatorExtensions.cs +++ b/Src/FluentAssertions/Equivalency/EnumerableEquivalencyValidatorExtensions.cs @@ -6,17 +6,6 @@ namespace FluentAssertions.Equivalency { internal static class EnumerableEquivalencyValidatorExtensions { - public static Continuation AssertCollectionsHaveSameCount(ICollection subject, ICollection expectation) - { - return AssertionScope.Current - .WithExpectation("Expected {context:subject} to be a collection with {0} item(s){reason}", expectation.Count) - .AssertEitherCollectionIsNotEmpty(subject, expectation) - .Then - .AssertCollectionHasEnoughItems(subject, expectation) - .Then - .AssertCollectionHasNotTooManyItems(subject, expectation); - } - public static Continuation AssertEitherCollectionIsNotEmpty(this AssertionScope scope, ICollection subject, ICollection expectation) { return scope diff --git a/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs index 7a577f1dd4..ff1b181dd5 100644 --- a/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs +++ b/Src/FluentAssertions/Equivalency/GenericDictionaryEquivalencyStep.cs @@ -184,7 +184,9 @@ private static Type GetIDictionaryInterface(Type expectedType) .FailWith("but has additional key(s) {0}", keyDifference.AdditionalKeys) .Then .ForCondition(!hasMissingKeys || !hasAdditionalKeys) - .FailWith("but it misses key(s) {0} and has additional key(s) {1}", keyDifference.MissingKeys, keyDifference.AdditionalKeys); + .FailWith("but it misses key(s) {0} and has additional key(s) {1}", keyDifference.MissingKeys, keyDifference.AdditionalKeys) + .Then + .ClearExpectation(); } private static KeyDifference CalculateKeyDifference + /// Clears the expectation set by . + /// + // SMELL: It would be better to give the expectation an explicit scope, but that would be a breaking change. + public Continuation ClearExpectation() + { + expectation = null; + + return new Continuation(this, true); + } + /// /// Allows to safely select the subject for successive assertions, even when the prior assertion has failed. /// diff --git a/Src/FluentAssertions/Execution/GivenSelector.cs b/Src/FluentAssertions/Execution/GivenSelector.cs index ab3e435075..c6e6db9bf6 100644 --- a/Src/FluentAssertions/Execution/GivenSelector.cs +++ b/Src/FluentAssertions/Execution/GivenSelector.cs @@ -131,5 +131,11 @@ public ContinuationOfGiven FailWith(string message, params object[] args) return new ContinuationOfGiven(this, succeeded); } + + public ContinuationOfGiven ClearExpectation() + { + parentScope.ClearExpectation(); + return new ContinuationOfGiven(this, parentScope.Succeeded); + } } } diff --git a/Src/FluentAssertions/Primitives/DateTimeAssertions.cs b/Src/FluentAssertions/Primitives/DateTimeAssertions.cs index 9421525fea..a00bbdfebe 100644 --- a/Src/FluentAssertions/Primitives/DateTimeAssertions.cs +++ b/Src/FluentAssertions/Primitives/DateTimeAssertions.cs @@ -386,7 +386,9 @@ public AndConstraint HaveYear(int expected, string because = .Then .ForCondition(Subject.Value.Year == expected) .BecauseOf(because, becauseArgs) - .FailWith(", but found {0}.", Subject.Value.Year); + .FailWith(", but found {0}.", Subject.Value.Year) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -438,7 +440,9 @@ public AndConstraint HaveMonth(int expected, string because .Then .ForCondition(Subject.Value.Month == expected) .BecauseOf(because, becauseArgs) - .FailWith(", but found {0}.", Subject.Value.Month); + .FailWith(", but found {0}.", Subject.Value.Month) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -464,7 +468,9 @@ public AndConstraint NotHaveMonth(int unexpected, string bec .Then .ForCondition(Subject.Value.Month != unexpected) .BecauseOf(because, becauseArgs) - .FailWith(", but it was."); + .FailWith(", but it was.") + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -490,7 +496,9 @@ public AndConstraint HaveDay(int expected, string because = .Then .ForCondition(Subject.Value.Day == expected) .BecauseOf(because, becauseArgs) - .FailWith(", but found {0}.", Subject.Value.Day); + .FailWith(", but found {0}.", Subject.Value.Day) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -516,7 +524,9 @@ public AndConstraint NotHaveDay(int unexpected, string becau .Then .ForCondition(Subject.Value.Day != unexpected) .BecauseOf(because, becauseArgs) - .FailWith(", but it was."); + .FailWith(", but it was.") + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -542,7 +552,9 @@ public AndConstraint HaveHour(int expected, string because = .Then .ForCondition(Subject.Value.Hour == expected) .BecauseOf(because, becauseArgs) - .FailWith(", but found {0}.", Subject.Value.Hour); + .FailWith(", but found {0}.", Subject.Value.Hour) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -568,8 +580,9 @@ public AndConstraint NotHaveHour(int unexpected, string beca .Then .ForCondition(Subject.Value.Hour != unexpected) .BecauseOf(because, becauseArgs) - .FailWith(", but it was.", unexpected, - Subject.Value.Hour); + .FailWith(", but it was.", unexpected, Subject.Value.Hour) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -596,7 +609,9 @@ public AndConstraint NotHaveHour(int unexpected, string beca .Then .ForCondition(Subject.Value.Minute == expected) .BecauseOf(because, becauseArgs) - .FailWith(", but found {0}.", Subject.Value.Minute); + .FailWith(", but found {0}.", Subject.Value.Minute) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -623,8 +638,9 @@ public AndConstraint NotHaveHour(int unexpected, string beca .Then .ForCondition(Subject.Value.Minute != unexpected) .BecauseOf(because, becauseArgs) - .FailWith(", but it was.", unexpected, - Subject.Value.Minute); + .FailWith(", but it was.", unexpected, Subject.Value.Minute) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -651,7 +667,9 @@ public AndConstraint NotHaveHour(int unexpected, string beca .Then .ForCondition(Subject.Value.Second == expected) .BecauseOf(because, becauseArgs) - .FailWith(", but found {0}.", Subject.Value.Second); + .FailWith(", but found {0}.", Subject.Value.Second) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -678,7 +696,8 @@ public AndConstraint NotHaveHour(int unexpected, string beca .Then .ForCondition(Subject.Value.Second != unexpected) .BecauseOf(because, becauseArgs) - .FailWith(", but it was."); + .FailWith(", but it was.") + .Then.ClearExpectation(); return new AndConstraint(this); } @@ -768,7 +787,9 @@ public DateTimeRangeAssertions BeLessThan(TimeSpan timeSpan) .Then .ForCondition(Subject.Value.Date == expectedDate) .BecauseOf(because, becauseArgs) - .FailWith(", but found {1}.", expectedDate, Subject.Value); + .FailWith(", but found {1}.", expectedDate, Subject.Value) + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -797,7 +818,9 @@ public DateTimeRangeAssertions BeLessThan(TimeSpan timeSpan) .Then .ForCondition(Subject.Value.Date != unexpectedDate) .BecauseOf(because, becauseArgs) - .FailWith(", but it was."); + .FailWith(", but it was.") + .Then + .ClearExpectation(); return new AndConstraint(this); } @@ -888,7 +911,9 @@ public AndConstraint BeIn(DateTimeKind expectedKind, string .Then .ForCondition(Subject.Value.Kind == expectedKind) .BecauseOf(because, becauseArgs) - .FailWith(", but found {0}.", Subject.Value.Kind); + .FailWith(", but found {0}.", Subject.Value.Kind) + .Then + .ClearExpectation(); return new AndConstraint(this); } diff --git a/Tests/Shared.Specs/CollectionEquivalencySpecs.cs b/Tests/Shared.Specs/CollectionEquivalencySpecs.cs index e34c9838e0..086a0c3cce 100644 --- a/Tests/Shared.Specs/CollectionEquivalencySpecs.cs +++ b/Tests/Shared.Specs/CollectionEquivalencySpecs.cs @@ -1142,6 +1142,46 @@ public void When_asserting_equivalence_of_non_generic_collections_it_should_resp .WithMessage("*Wheels*not have*VehicleId*not have*"); } + [Fact] + public void When_comparing_against_a_non_generic_collection_it_should_treat_it_as_unordered_collection_of_objects() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + List actual = new List { typeof(int), typeof(string) }; + IEnumerable expectation = new List { typeof(string), typeof(int) }; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => actual.Should().BeEquivalentTo(expectation); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().NotThrow(); + } + + [Fact] + public void When_comparing_against_a_non_generic_collection_it_should_treat_it_as_collection_of_objects() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + List actual = new List { typeof(int), typeof(string) }; + IEnumerable expectation = new List { typeof(string), typeof(int) }; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => actual.Should().BeEquivalentTo(expectation, o => o.WithStrictOrdering().WithTracing()); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().NotThrow(); + } + [Fact] public void When_custom_assertion_rules_are_utilized_the_rules_should_be_respected() {