diff --git a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs index 54a10c802c..0c369091d0 100644 --- a/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +++ b/Src/FluentAssertions/Collections/GenericCollectionAssertions.cs @@ -2671,6 +2671,66 @@ public AndConstraint OnlyHaveUniqueItems(string because = "", param return new AndConstraint((TAssertions)this); } + /// + /// Asserts that a collection contains only items which meet + /// the criteria provided by the inspector. + /// + /// + /// The element inspector, which inspects each element in turn. + /// + /// + /// A 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 . + /// + /// is null. + public AndConstraint AllSatisfy(Action expected, string because = "", params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(expected, nameof(expected), "Cannot verify against a inspector"); + + bool success = Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:collection} to contain only items satisfying the inspector{reason}, ") + .Given(() => Subject) + .ForCondition(subject => subject is not null) + .FailWith("but collection is .") + .Then + .ForCondition(subject => subject.Any()) + .FailWith("but collection is empty.") + .Then + .ClearExpectation(); + + if (success) + { + string[] failuresFromInspectors; + + using (CallerIdentifier.OverrideStackSearchUsingCurrentScope()) + { + var elementInspectors = Subject.Select(_ => expected); + failuresFromInspectors = CollectFailuresFromInspectors(elementInspectors); + } + + if (failuresFromInspectors.Any()) + { + string failureMessage = Environment.NewLine + + string.Join(Environment.NewLine, failuresFromInspectors.Select(x => x.IndentLines())); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .WithExpectation("Expected {context:collection} to contain only items satisfying the inspector{reason}:") + .FailWithPreFormatted(failureMessage) + .Then + .ClearExpectation(); + } + + return new AndConstraint((TAssertions)this); + } + + return new AndConstraint((TAssertions)this); + } + /// /// Asserts that a collection contains exactly a given number of elements, which meet /// the criteria provided by the element inspectors. diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index b449218afb..b583bd2294 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -392,6 +392,7 @@ namespace FluentAssertions.Collections public FluentAssertions.AndConstraint AllBeEquivalentTo(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint AllBeOfType(System.Type expectedType, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint> AllBeOfType(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint AllSatisfy(System.Action expected, string because = "", params object[] becauseArgs) { } protected void AssertCollectionEndsWith(System.Collections.Generic.IEnumerable actual, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertCollectionStartsWith(System.Collections.Generic.IEnumerable actualItems, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertSubjectEquality(System.Collections.Generic.IEnumerable expectation, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt index d5e90c248e..da1f41efbc 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -392,6 +392,7 @@ namespace FluentAssertions.Collections public FluentAssertions.AndConstraint AllBeEquivalentTo(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint AllBeOfType(System.Type expectedType, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint> AllBeOfType(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint AllSatisfy(System.Action expected, string because = "", params object[] becauseArgs) { } protected void AssertCollectionEndsWith(System.Collections.Generic.IEnumerable actual, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertCollectionStartsWith(System.Collections.Generic.IEnumerable actualItems, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertSubjectEquality(System.Collections.Generic.IEnumerable expectation, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt index ee25dd44c0..7f464a2eb9 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -392,6 +392,7 @@ namespace FluentAssertions.Collections public FluentAssertions.AndConstraint AllBeEquivalentTo(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint AllBeOfType(System.Type expectedType, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint> AllBeOfType(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint AllSatisfy(System.Action expected, string because = "", params object[] becauseArgs) { } protected void AssertCollectionEndsWith(System.Collections.Generic.IEnumerable actual, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertCollectionStartsWith(System.Collections.Generic.IEnumerable actualItems, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertSubjectEquality(System.Collections.Generic.IEnumerable expectation, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt index e9d45913dc..584de8abbc 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -385,6 +385,7 @@ namespace FluentAssertions.Collections public FluentAssertions.AndConstraint AllBeEquivalentTo(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint AllBeOfType(System.Type expectedType, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint> AllBeOfType(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint AllSatisfy(System.Action expected, string because = "", params object[] becauseArgs) { } protected void AssertCollectionEndsWith(System.Collections.Generic.IEnumerable actual, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertCollectionStartsWith(System.Collections.Generic.IEnumerable actualItems, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertSubjectEquality(System.Collections.Generic.IEnumerable expectation, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt index 84a9aa9c4e..2b7eacf3e5 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -392,6 +392,7 @@ namespace FluentAssertions.Collections public FluentAssertions.AndConstraint AllBeEquivalentTo(TExpectation expectation, System.Func, FluentAssertions.Equivalency.EquivalencyAssertionOptions> config, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint AllBeOfType(System.Type expectedType, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint> AllBeOfType(string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint AllSatisfy(System.Action expected, string because = "", params object[] becauseArgs) { } protected void AssertCollectionEndsWith(System.Collections.Generic.IEnumerable actual, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertCollectionStartsWith(System.Collections.Generic.IEnumerable actualItems, System.Collections.Generic.ICollection expected, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } protected void AssertSubjectEquality(System.Collections.Generic.IEnumerable expectation, System.Func equalityComparison, string because = "", params object[] becauseArgs) { } diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.AllSatisfy.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.AllSatisfy.cs new file mode 100644 index 0000000000..ed82cafcf5 --- /dev/null +++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.AllSatisfy.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions.Execution; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Specs.Collections +{ + /// + /// The AllSatisfy specs. + /// + public partial class CollectionAssertionSpecs + { + public class AllSatisfy + { + [Fact] + public void A_null_inspector_should_throw() + { + // Arrange + IEnumerable collection = new[] { 1, 2 }; + + // Act + Action act = () => collection.Should().AllSatisfy(null); + + // Assert + act.Should() + .Throw() + .WithMessage("Cannot verify against a inspector*"); + } + + [Fact] + public void A_null_collection_should_throw() + { + // Arrange + IEnumerable collection = null; + + // Act + Action act = () => + { + using var _ = new AssertionScope(); + collection.Should().AllSatisfy(x => x.Should().Be(1), "because we want to test the failure {0}", "message"); + }; + + // Assert + act.Should() + .Throw() + .WithMessage( + "Expected collection to contain only items satisfying the inspector because we want to test the failure message, but collection is ."); + } + + [Fact] + public void An_empty_collection_should_throw() + { + // Arrange + var collection = Enumerable.Empty(); + + // Act + Action act = () + => collection.Should() + .AllSatisfy(x => x.Should().Be(1), "because we want to test the failure {0}", "message"); + + // Assert + act.Should() + .Throw() + .WithMessage( + "Expected collection to contain only items satisfying the inspector because we want to test the failure message, but collection is empty."); + } + + [Fact] + public void All_items_satisfying_inspector_should_succeed() + { + // Arrange + var collection = new[] { new Customer { Age = 21, Name = "John" }, new Customer { Age = 21, Name = "Jane" } }; + + // Act / Assert + collection.Should().AllSatisfy(x => x.Age.Should().Be(21)); + } + + [Fact] + public void Any_items_not_satisfying_inspector_should_throw() + { + // Arrange + var customers = new[] + { + new CustomerWithItems { Age = 21, Items = new[] { 1, 2 } }, + new CustomerWithItems { Age = 22, Items = new[] { 3 } } + }; + + // Act + Action act = () => customers.Should() + .AllSatisfy( + customer => + { + customer.Age.Should().BeLessThan(21); + customer.Items.Should() + .AllSatisfy(item => item.Should().Be(3)); + }, + "because we want to test {0}", + "nested assertions"); + + // Assert + act.Should() + .Throw() + .WithMessage( + @"Expected customers to contain only items satisfying the inspector because we want to test nested assertions: +*At index 0: +*Expected customer.Age to be less than 21, but found 21 +*Expected customer.Items to contain only items satisfying the inspector: +*At index 0: +*Expected item to be 3, but found 1 +*At index 1: +*Expected item to be 3, but found 2 +*At index 1: +*Expected customer.Age to be less than 21, but found 22" + ); + } + + [Fact] + public void Inspector_message_that_is_not_reformatable_should_not_throw() + { + // Arrange + byte[][] subject = { new byte[] { 1 } }; + + // Act + Action act = () => subject.Should().AllSatisfy(e => e.Should().BeEquivalentTo(new byte[] { 2, 3, 4 })); + + // Assert + act.Should().NotThrow(); + } + } + } +} diff --git a/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.AllSatisfy.cs b/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.AllSatisfy.cs new file mode 100644 index 0000000000..b688dcb148 --- /dev/null +++ b/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.AllSatisfy.cs @@ -0,0 +1,44 @@ +using System; +using Xunit; +using Xunit.Sdk; + +namespace FluentAssertions.Specs.Collections +{ + public partial class GenericCollectionAssertionOfStringSpecs + { + public class AllSatisfy + { + [Fact] + public void All_items_satisfying_inspector_should_succeed() + { + // Arrange + string[] collection = new[] { "John", "John" }; + + // Act / Assert + collection.Should().AllSatisfy(value => value.Should().Be("John")); + } + + [Fact] + public void Any_items_not_satisfying_inspector_should_throw() + { + // Arrange + string[] collection = new[] { "Jack", "Jessica" }; + + // Act + Action act = () => collection.Should() + .AllSatisfy( + value => value.Should().Be("John"), + "because we want to test the failure {0}", + "message"); + + // Assert + act.Should() + .Throw() + .WithMessage( + "Expected collection to contain only items satisfying the inspector because we want to test the failure message:" + + "*John*Jack" + + "*John*Jessica*"); + } + } + } +} diff --git a/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs b/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs index bc5ececcc1..200a6be64a 100644 --- a/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs +++ b/Tests/FluentAssertions.Specs/Collections/GenericCollectionAssertionOfStringSpecs.cs @@ -9,7 +9,7 @@ namespace FluentAssertions.Specs.Collections { - public class GenericCollectionAssertionOfStringSpecs + public partial class GenericCollectionAssertionOfStringSpecs { [Fact] public void Should_fail_when_asserting_collection_has_a_count_that_is_different_from_the_number_of_items() diff --git a/docs/_pages/collections.md b/docs/_pages/collections.md index 4874c4a858..bd4905ca10 100644 --- a/docs/_pages/collections.md +++ b/docs/_pages/collections.md @@ -190,6 +190,22 @@ collection.Should().SatisfyRespectively( }); ``` +If you need to perform the same assertion on all elements of a collection: + +```csharp +var collection = new [] +{ + new { Id = 1, Name = "John", Attributes = new string[] { } }, + new { Id = 2, Name = "Jane", Attributes = new string[] { "attr" } } +}; +collection.Should().AllSatisfy(x => +{ + x.Id.Should().BePositive(); + x.Name.Should().StartWith("J"); + x.Attributes.Should().NotBeNull(); +}); +``` + If you need to perform individual assertions on all elements of a collection without setting expectation about the order of elements: ```csharp diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 1b625525d9..1745b6b0c6 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -10,6 +10,7 @@ sidebar: ## Unreleased ### What's New +* Added `AllSatisfy` for asserting all items in a collection satisfy an inspector - [#1790](https://github.com/fluentassertions/fluentassertions/pull/1790) * Added `WithMapping` option to `BeEquivalentTo` to map members with different names between the subject and expectation - [#1742](https://github.com/fluentassertions/fluentassertions/pull/1742) ### Fixes