From 0702ef403998b3d87abe447e3a2f9875a457ec50 Mon Sep 17 00:00:00 2001 From: Matthias Lischka Date: Sat, 15 Dec 2018 09:01:58 +0100 Subject: [PATCH] Add collection assertion ContainEquivalentOf (#950) --- .../Collections/CollectionAssertions.cs | 92 +++++++ .../Shared.Specs/CollectionAssertionSpecs.cs | 245 ++++++++++++++++++ docs/_pages/documentation.md | 5 + 3 files changed, 342 insertions(+) diff --git a/Src/FluentAssertions/Collections/CollectionAssertions.cs b/Src/FluentAssertions/Collections/CollectionAssertions.cs index 331f164764..0876ed13fb 100644 --- a/Src/FluentAssertions/Collections/CollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/CollectionAssertions.cs @@ -534,6 +534,98 @@ public AndConstraint BeEquivalentTo(IEnumerable expectation, string return new AndConstraint((TAssertions)this); } + /// + /// Asserts that a collection of objects contains at least one object equivalent to another object. + /// + /// + /// Objects within the collection are equivalent to the expected object 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. + /// 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 AndConstraint ContainEquivalentOf(TExpectation expectation, string because = "", + params object[] becauseArgs) + { + return ContainEquivalentOf(expectation, config => config, because, becauseArgs); + } + + /// + /// Asserts that a collection of objects contains at least one object equivalent to another object. + /// + /// + /// Objects within the collection are equivalent to the expected object 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. + /// 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 ContainEquivalentOf(TExpectation expectation, Func, + EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) + { + if (ReferenceEquals(Subject, null)) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:collection} to contain equivalent of {0}{reason}, but found .", expectation); + } + + IEquivalencyAssertionOptions options = config(AssertionOptions.CloneDefaults()); + IEnumerable actualItems = Subject.Cast(); + + using (var scope = new AssertionScope()) + { + scope.AddReportable("configuration", options.ToString()); + + foreach (object actualItem in actualItems) + { + var context = new EquivalencyValidationContext + { + Subject = actualItem, + Expectation = expectation, + CompileTimeType = typeof(TExpectation), + Because = because, + BecauseArgs = becauseArgs, + Tracer = options.TraceWriter, + }; + + var equivalencyValidator = new EquivalencyValidator(options); + equivalencyValidator.AssertEquality(context); + + string[] failures = scope.Discard(); + + if (!failures.Any()) + { + return new AndConstraint((TAssertions)this); + } + } + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:collection} {0} to contain equivalent of {1}.", Subject, expectation); + } + + return new AndConstraint((TAssertions)this); + } + /// /// Asserts that the current collection only contains items that are assignable to the type . /// diff --git a/Tests/Shared.Specs/CollectionAssertionSpecs.cs b/Tests/Shared.Specs/CollectionAssertionSpecs.cs index 6beb5df2b1..8858c94952 100644 --- a/Tests/Shared.Specs/CollectionAssertionSpecs.cs +++ b/Tests/Shared.Specs/CollectionAssertionSpecs.cs @@ -2336,6 +2336,251 @@ public void When_testing_collections_not_to_be_equivalent_against_same_collectio #endregion + #region Contain Equivalent Of + + [Fact] + public void When_collection_contains_object_equal_of_another_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + var item = new Customer { Name = "John" }; + IEnumerable collection = new[] { new Customer { Name = "Jane" }, item }; + + //----------------------------------------------------------------------------------------------------------- + // Act / Assert + //----------------------------------------------------------------------------------------------------------- + collection.Should().ContainEquivalentOf(item); + } + + [Fact] + public void When_collection_contains_object_equivalent_of_another_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = new[] { new Customer { Name = "Jane" }, new Customer { Name = "John" } }; + var item = new Customer { Name = "John" }; + + //----------------------------------------------------------------------------------------------------------- + // Act / Assert + //----------------------------------------------------------------------------------------------------------- + collection.Should().ContainEquivalentOf(item); + } + + [Fact] + public void When_character_collection_does_contain_equivalent_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + char[] collection = ("abc123ab").ToCharArray(); + char item = 'c'; + + //----------------------------------------------------------------------------------------------------------- + // Act / Assert + //----------------------------------------------------------------------------------------------------------- + collection.Should().ContainEquivalentOf(item); + } + + [Fact] + public void When_string_collection_does_contain_same_string_with_other_case_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + string[] collection = new[] { "a", "b", "c" }; + string item = "C"; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => collection.Should().ContainEquivalentOf(item); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw().WithMessage("Expected collection {\"a\", \"b\", \"c\"} to contain equivalent of \"C\".*"); + } + + [Fact] + public void When_collection_does_not_contain_object_equivalent_of_another_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = new[] { 1, 2, 3 }; + int item = 4; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => collection.Should().ContainEquivalentOf(item); ; + + //----------------------------------------------------------------------------------------------------------- + // Act / Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw().WithMessage("Expected collection {1, 2, 3} to contain equivalent of 4.*"); + } + + [Fact] + public void When_asserting_collection_to_contain_equivalent_but_collection_is_null_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = null; + int expectation = 1; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => + collection.Should().ContainEquivalentOf(expectation, "because we want to test the behaviour with a null subject"); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw().WithMessage( + "Expected collection to contain equivalent of 1 because we want to test the behaviour with a null subject, but found ."); + } + + [Fact] + public void When_collection_contains_equivalent_null_object_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = new[] { 1, 2, 3, (int?)null }; + int? item = null; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => collection.Should().ContainEquivalentOf(item); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().NotThrow(); + } + + [Fact] + public void When_collection_does_not_contain_equivalent_null_object_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = new[] { 1, 2, 3 }; + int? item = null; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => collection.Should().ContainEquivalentOf(item); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw().WithMessage("Expected collection {1, 2, 3} to contain equivalent of .*"); + } + + [Fact] + public void When_empty_collection_does_not_contain_equivalent_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = new int[0]; + int item = 1; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => collection.Should().ContainEquivalentOf(item); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw().WithMessage("Expected collection {empty} to contain equivalent of 1.*"); + } + + [Fact] + public void When_collection_does_not_contain_equivalent_because_of_second_property_it_should_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable subject = new [] + { + new Customer + { + Name = "John", + Age = 18 + }, + new Customer + { + Name = "Jane", + Age = 18 + } + }; + var item = new Customer { Name = "John", Age = 20 }; + + //----------------------------------------------------------------------------------------------------------- + // Act + //----------------------------------------------------------------------------------------------------------- + Action act = () => subject.Should().ContainEquivalentOf(item); + + //----------------------------------------------------------------------------------------------------------- + // Assert + //----------------------------------------------------------------------------------------------------------- + act.Should().Throw(); + } + + [Fact] + public void When_collection_does_contain_equivalent_by_including_single_property_it_should_not_throw() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = new [] + { + new Customer + { + Name = "John", + Age = 18 + }, + new Customer + { + Name = "Jane", + Age = 18 + } + }; + var item = new Customer { Name = "John", Age = 20 }; + + //----------------------------------------------------------------------------------------------------------- + // Act / Assert + //----------------------------------------------------------------------------------------------------------- + collection.Should().ContainEquivalentOf(item, options => options.Including(x => x.Name)); + } + + [Fact] + public void When_collection_contains_object_equivalent_of_boxed_object_it_should_succeed() + { + //----------------------------------------------------------------------------------------------------------- + // Arrange + //----------------------------------------------------------------------------------------------------------- + IEnumerable collection = new[] { 1, 2, 3 }; + object boxedValue = 2; + + //----------------------------------------------------------------------------------------------------------- + // Act / Assert + //----------------------------------------------------------------------------------------------------------- + collection.Should().ContainEquivalentOf(boxedValue); + } + + #endregion + #region Be Subset Of [Fact] diff --git a/docs/_pages/documentation.md b/docs/_pages/documentation.md index 223ff679cb..8336709028 100644 --- a/docs/_pages/documentation.md +++ b/docs/_pages/documentation.md @@ -461,6 +461,9 @@ collection.Should().NotContain(new[] { 82, 83 }); collection.Should().NotContainNulls(); collection.Should().NotContain(x => x > 10); +object boxedValue = 2; +collection.Should().ContainEquivalentOf(boxedValue); // Compared by object equivalence + const int successor = 5; const int predecessor = 5; collection.Should().HaveElementPreceding(successor, element); @@ -481,6 +484,8 @@ collection.Should().NotBeAscendingInOrder(); collection.Should().NotBeDescendingInOrder(); ``` +The `collection.Should().ContainEquivalentOf(boxedValue)` asserts that a collection contains at least one object that is equivalent to the expected object. The comparison is governed by the same rules and options as the [Object graph comparison](#object-graph-comparison). + Those last two methods can be used to assert a collection contains items in ascending or descending order. For simple types that might be fine, but for more complex types, it requires you to implement `IComparable`, something that doesn't make a whole lot of sense in all cases. That's why we offer overloads that take an expression.