diff --git a/Src/FluentAssertions/Xml/XDocumentAssertions.cs b/Src/FluentAssertions/Xml/XDocumentAssertions.cs index dd41a59357..668e5cebfb 100644 --- a/Src/FluentAssertions/Xml/XDocumentAssertions.cs +++ b/Src/FluentAssertions/Xml/XDocumentAssertions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Xml; using System.Xml.Linq; @@ -132,7 +134,7 @@ public AndConstraint NotBeEquivalentTo(XDocument unexpected params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(expected, nameof(expected), - "Cannot assert the document has a root element if the expected name is *"); + "Cannot assert the document has a root element if the expected name is ."); return HaveRoot(XNamespace.None + expected, because, becauseArgs); } @@ -157,7 +159,7 @@ public AndConstraint NotBeEquivalentTo(XDocument unexpected } Guard.ThrowIfArgumentIsNull(expected, nameof(expected), - "Cannot assert the document has a root element if the expected name is *"); + "Cannot assert the document has a root element if the expected name is ."); XElement root = Subject.Root; @@ -176,7 +178,7 @@ public AndConstraint NotBeEquivalentTo(XDocument unexpected /// child element with the specified name. /// /// - /// The name of the expected child element of the current document's Root element. + /// The name of the expected child element of the current document's element. /// /// /// A formatted phrase as is supported by explaining why the assertion @@ -189,17 +191,43 @@ public AndConstraint NotBeEquivalentTo(XDocument unexpected params object[] becauseArgs) { Guard.ThrowIfArgumentIsNull(expected, nameof(expected), - "Cannot assert the document has an element if the expected name is *"); + "Cannot assert the document has an element if the expected name is ."); return HaveElement(XNamespace.None + expected, because, becauseArgs); } + /// + /// Asserts that the element of the current has the specified occurrence of + /// child elements with the specified name. + /// + /// + /// The name of the expected child element of the current document's element. + /// + /// + /// A constraint specifying the number of times the specified elements should appear. + /// + /// + /// 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 . + /// + public AndWhichConstraint> HaveElement(string expected, + OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(expected, nameof(expected), + "Cannot assert the document has an element if the expected name is ."); + + return HaveElement(XNamespace.None + expected, occurrenceConstraint, because, becauseArgs); + } + /// /// Asserts that the element of the current has a direct /// child element with the specified name. /// /// - /// The full name of the expected child element of the current document's Root element. + /// The full name of the expected child element of the current document's element. /// /// /// A formatted phrase as is supported by explaining why the assertion @@ -217,7 +245,7 @@ public AndConstraint NotBeEquivalentTo(XDocument unexpected } Guard.ThrowIfArgumentIsNull(expected, nameof(expected), - "Cannot assert the document has an element if the expected name is *"); + "Cannot assert the document has an element if the expected name is ."); Execute.Assertion .ForCondition(Subject.Root is not null) @@ -237,6 +265,65 @@ public AndConstraint NotBeEquivalentTo(XDocument unexpected return new AndWhichConstraint(this, xElement); } + /// + /// Asserts that the element of the current has the specified occurrence of + /// child elements with the specified name. + /// + /// + /// The full name of the expected child element of the current document's element. + /// + /// + /// A constraint specifying the number of times the specified elements should appear. + /// + /// + /// 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 . + /// + public AndWhichConstraint> HaveElement(XName expected, + OccurrenceConstraint occurrenceConstraint, string because = "", + params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(expected, nameof(expected), + "Cannot assert the document has an element count if the element name is ."); + + bool success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Cannot assert the count if the document itself is ."); + + IEnumerable xElements = Enumerable.Empty(); + + if (success) + { + var root = Subject.Root; + success = Execute.Assertion + .ForCondition(root is not null) + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:subject} to have root element containing a child {0}{reason}, but it has no root element.", + expected.ToString()); + + if (success) + { + xElements = root.Elements(expected); + int actual = xElements.Count(); + + Execute.Assertion + .ForConstraint(occurrenceConstraint, actual) + .BecauseOf(because, becauseArgs) + .FailWith( + $"Expected {{context:subject}} to have a root element containing a child {{0}} " + + $"{{expectedOccurrence}}{{reason}}, but found it {actual.Times()}.", + expected.ToString()); + } + } + + return new AndWhichConstraint>(this, xElements); + } + /// /// Returns the type of the subject the assertion applies on. /// diff --git a/Src/FluentAssertions/Xml/XElementAssertions.cs b/Src/FluentAssertions/Xml/XElementAssertions.cs index c0f2a0d4cb..4e9b6e2876 100644 --- a/Src/FluentAssertions/Xml/XElementAssertions.cs +++ b/Src/FluentAssertions/Xml/XElementAssertions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Xml; using System.Xml.Linq; using FluentAssertions.Common; @@ -288,6 +290,82 @@ public AndConstraint HaveValue(string expected, string becau return new AndWhichConstraint(this, xElement); } + /// + /// Asserts that the of the current has the specified occurrence of + /// child elements with the specified name. + /// + /// + /// The full name of the expected child element of the current element's . + /// + /// + /// A constraint specifying the number of times the specified elements should appear. + /// + /// + /// 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 . + /// + public AndWhichConstraint> HaveElement(XName expected, + OccurrenceConstraint occurrenceConstraint, string because = "", + params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(expected, nameof(expected), + "Cannot assert the element has an element count if the element name is ."); + + bool success = Execute.Assertion + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith( + "Expected {context:subject} to have an element with count of {0}{reason}, but the element itself is .", + expected.ToString()); + + IEnumerable xElements = Enumerable.Empty(); + + if (success) + { + xElements = Subject.Elements(expected); + int actual = xElements.Count(); + + Execute.Assertion + .ForConstraint(occurrenceConstraint, actual) + .BecauseOf(because, becauseArgs) + .FailWith( + $"Expected {{context:subject}} to have an element {{0}} {{expectedOccurrence}}" + + $"{{reason}}, but found it {actual.Times()}.", + expected.ToString()); + } + + return new AndWhichConstraint>(this, xElements); + } + + /// + /// Asserts that the of the current has the specified occurrence of + /// child elements with the specified name. + /// + /// + /// The name of the expected child element of the current element's . + /// + /// + /// A constraint specifying the number of times the specified elements should appear. + /// + /// + /// 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 . + /// + public AndWhichConstraint> HaveElement(string expected, + OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) + { + Guard.ThrowIfArgumentIsNull(expected, nameof(expected), + "Cannot assert the element has an element if the expected name is ."); + + return HaveElement(XNamespace.None + expected, occurrenceConstraint, because, becauseArgs); + } + /// /// Returns the type of the subject the assertion applies on. /// diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt index a86a4adf5e..8a2d1f8e14 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net47.verified.txt @@ -2685,6 +2685,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint BeEquivalentTo(System.Xml.Linq.XDocument expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XDocument unexpected, string because = "", params object[] becauseArgs) { } @@ -2700,6 +2702,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint HaveAttribute(System.Xml.Linq.XName expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveValue(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEquivalentTo(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt index 45386470fb..1b6faed215 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt @@ -2805,6 +2805,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint BeEquivalentTo(System.Xml.Linq.XDocument expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XDocument unexpected, string because = "", params object[] becauseArgs) { } @@ -2820,6 +2822,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint HaveAttribute(System.Xml.Linq.XName expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveValue(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEquivalentTo(System.Xml.Linq.XElement unexpected, 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 32f7082f3f..a9f68eb68d 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp2.1.verified.txt @@ -2687,6 +2687,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint BeEquivalentTo(System.Xml.Linq.XDocument expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XDocument unexpected, string because = "", params object[] becauseArgs) { } @@ -2702,6 +2704,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint HaveAttribute(System.Xml.Linq.XName expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveValue(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEquivalentTo(System.Xml.Linq.XElement unexpected, 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 cd16c72a15..8c7f8490bc 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netcoreapp3.0.verified.txt @@ -2687,6 +2687,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint BeEquivalentTo(System.Xml.Linq.XDocument expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XDocument unexpected, string because = "", params object[] becauseArgs) { } @@ -2702,6 +2704,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint HaveAttribute(System.Xml.Linq.XName expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveValue(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEquivalentTo(System.Xml.Linq.XElement unexpected, 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 e7ef120517..1fc893363c 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.0.verified.txt @@ -2637,6 +2637,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint BeEquivalentTo(System.Xml.Linq.XDocument expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XDocument unexpected, string because = "", params object[] becauseArgs) { } @@ -2652,6 +2654,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint HaveAttribute(System.Xml.Linq.XName expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveValue(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEquivalentTo(System.Xml.Linq.XElement unexpected, 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 f17275a93c..dedd54bad1 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions/netstandard2.1.verified.txt @@ -2687,6 +2687,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint BeEquivalentTo(System.Xml.Linq.XDocument expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveRoot(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XDocument unexpected, string because = "", params object[] becauseArgs) { } @@ -2702,6 +2704,8 @@ namespace FluentAssertions.Xml public FluentAssertions.AndConstraint HaveAttribute(System.Xml.Linq.XName expectedName, string expectedValue, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(System.Xml.Linq.XName expected, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndWhichConstraint> HaveElement(System.Xml.Linq.XName expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint HaveValue(string expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBe(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint NotBeEquivalentTo(System.Xml.Linq.XElement unexpected, string because = "", params object[] becauseArgs) { } diff --git a/Tests/FluentAssertions.Specs/Xml/XDocumentAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Xml/XDocumentAssertionSpecs.cs index e7c9767ad4..837a6b0422 100644 --- a/Tests/FluentAssertions.Specs/Xml/XDocumentAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Xml/XDocumentAssertionSpecs.cs @@ -1,5 +1,6 @@ using System; using System.Xml.Linq; +using FluentAssertions.Execution; using FluentAssertions.Formatting; using Xunit; using Xunit.Sdk; @@ -1158,5 +1159,170 @@ public void When_asserting_a_document_has_an_element_with_a_null_xname_it_should } #endregion + + #region HaveElement (with occurrence) + + [Fact] + public void When_asserting_document_has_two_child_elements_and_it_does_it_succeeds() + { + // Arrange + var document = XDocument.Parse( + @" + + + "); + + // Act / Assert + document.Should().HaveElement("child", Exactly.Twice()); + } + + [Fact] + public void Asserting_document_null_inside_an_assertion_scope_it_checks_the_whole_assertion_scope_before_failing() + { + // Arrange + XDocument document = null; + + // Act + Action act = () => + { + using (new AssertionScope()) + { + document.Should().HaveElement("child", Exactly.Twice()); + document.Should().HaveElement("child", Exactly.Twice()); + } + }; + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void Asserting_with_document_root_null_inside_an_assertion_scope_it_checks_the_whole_assertion_scope_before_failing() + { + // Arrange + XDocument document = new(); + + // Act + Action act = () => + { + using (new AssertionScope()) + { + document.Should().HaveElement("child", Exactly.Twice()); + document.Should().HaveElement("child", Exactly.Twice()); + } + }; + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_asserting_document_has_two_child_elements_but_it_does_have_three_it_fails() + { + // Arrange + var document = XDocument.Parse( + @" + + + + "); + + // Act + Action act = () => document.Should().HaveElement("child", Exactly.Twice()); + + // Assert + act.Should().Throw() + .WithMessage("Expected document to have a root element containing a child \"child\"*exactly*2 times, but found it 3 times*"); + } + + [Fact] + public void Document_is_valid_and_expected_null_with_string_overload_it_fails() + { + // Arrange + var document = XDocument.Parse( + @" + + + + "); + + // Act + Action act = () => document.Should().HaveElement(null, Exactly.Twice()); + + // Assert + act.Should().Throw().WithMessage( + "Cannot assert the document has an element if the expected name is .*"); + } + + [Fact] + public void Document_is_valid_and_expected_null_with_x_name_overload_it_fails() + { + // Arrange + var document = XDocument.Parse( + @" + + + + "); + + // Act + Action act = () => document.Should().HaveElement((XName)null, Exactly.Twice()); + + // Assert + act.Should().Throw().WithMessage( + "Cannot assert the document has an element count if the element name is .*"); + } + + [Fact] + public void Chaining_after_a_successful_occurrence_check_does_continue_the_assertion() + { + // Arrange + var document = XDocument.Parse( + @" + + + + "); + + // Act / Assert + document.Should().HaveElement("child", AtLeast.Twice()) + .Which.Should().NotBeNull(); + } + + [Fact] + public void Chaining_after_a_non_successful_occurrence_check_does_not_continue_the_assertion() + { + // Arrange + var document = XDocument.Parse( + @" + + + + "); + + // Act + Action act = () => document.Should().HaveElement("child", Exactly.Once()) + .Which.Should().NotBeNull(); + + // Assert + act.Should().Throw() + .WithMessage("Expected document to have a root element containing a child \"child\"*exactly*1 time, but found it 3 times."); + } + + [Fact] + public void When_asserting_a_null_document_to_have_an_element_count_it_should_fail() + { + // Arrange + XDocument xDocument = null; + + // Act + Action act = () => xDocument.Should().HaveElement("child", AtLeast.Once()); + + // Assert + act.Should().Throw().WithMessage( + "Cannot assert the count if the document itself is ."); + } + + #endregion } } diff --git a/Tests/FluentAssertions.Specs/Xml/XElementAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Xml/XElementAssertionSpecs.cs index 3f3364b47a..176bd01d27 100644 --- a/Tests/FluentAssertions.Specs/Xml/XElementAssertionSpecs.cs +++ b/Tests/FluentAssertions.Specs/Xml/XElementAssertionSpecs.cs @@ -1,5 +1,6 @@ using System; using System.Xml.Linq; +using FluentAssertions.Execution; using Xunit; using Xunit.Sdk; @@ -1228,5 +1229,150 @@ public void When_asserting_element_has_a_child_element_with_an_empty_name_it_sho } #endregion + + #region HaveElement (with occurrence) + + [Fact] + public void Element_has_two_child_elements_and_it_expected_does_it_succeeds() + { + // Arrange + var element = XElement.Parse( + @" + + + "); + + // Act / Assert + element.Should().HaveElement("child", Exactly.Twice()); + } + + [Fact] + public void Asserting_element_inside_an_assertion_scope_it_checks_the_whole_assertion_scope_before_failing() + { + // Arrange + XElement element = null; + + // Act + Action act = () => + { + using (new AssertionScope()) + { + element.Should().HaveElement("child", Exactly.Twice()); + element.Should().HaveElement("child", Exactly.Twice()); + } + }; + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void Element_has_two_child_elements_and_three_expected_it_fails() + { + // Arrange + var element = XElement.Parse( + @" + + + + "); + + // Act + Action act = () => element.Should().HaveElement("child", Exactly.Twice()); + + // Assert + act.Should().Throw() + .WithMessage("Expected element to have an element \"child\"*exactly*2 times, but found it 3 times."); + } + + [Fact] + public void Element_is_valid_and_expected_null_with_string_overload_it_fails() + { + // Arrange + var element = XElement.Parse( + @" + + + + "); + + // Act + Action act = () => element.Should().HaveElement(null, Exactly.Twice()); + + // Assert + act.Should().Throw().WithMessage( + "Cannot assert the element has an element if the expected name is .*"); + } + + [Fact] + public void Element_is_valid_and_expected_null_with_x_name_overload_it_fails() + { + // Arrange + var element = XElement.Parse( + @" + + + + "); + + // Act + Action act = () => element.Should().HaveElement((XName)null, Exactly.Twice()); + + // Assert + act.Should().Throw().WithMessage( + "Cannot assert the element has an element count if the element name is .*"); + } + + [Fact] + public void Chaining_after_a_successful_occurrence_check_does_continue_the_assertion() + { + // Arrange + var element = XElement.Parse( + @" + + + + "); + + // Act / Assert + element.Should().HaveElement("child", AtLeast.Twice()) + .Which.Should().NotBeNull(); + } + + [Fact] + public void Chaining_after_a_non_successful_occurrence_check_does_not_continue_the_assertion() + { + // Arrange + var element = XElement.Parse( + @" + + + + "); + + // Act + Action act = () => element.Should().HaveElement("child", Exactly.Once()) + .Which.Should().NotBeNull(); + + // Assert + act.Should().Throw() + .WithMessage("Expected element to have an element \"child\"*exactly*1 time, but found it 3 times."); + } + + [Fact] + public void Null_element_is_expected_to_have_an_element_count_it_should_fail() + { + // Arrange + XElement xElement = null; + + // Act + Action act = () => xElement.Should().HaveElement("child", AtLeast.Once()); + + // Assert + act.Should().Throw().WithMessage( + "Expected* to have an element with count of *, but the element itself is ."); + } + + #endregion } } diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md index 4953f2f24f..3011877865 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -14,6 +14,7 @@ sidebar: * Added the ability to exclude fields & properties marked as non-browsable in the code editor from structural equality comparisons - [#1807](https://github.com/fluentassertions/fluentassertions/pull/1807) & [#1812](https://github.com/fluentassertions/fluentassertions/pull/1812) * Assertions on the collection types in System.Data (`DataSet.Tables`, `DataTable.Columns`, `DataTable.Rows`) have been restored - [#1812](https://github.com/fluentassertions/fluentassertions/pull/1812) * Add `For`/`Exclude` to allow exclusion of members inside a collection - [#1782](https://github.com/fluentassertions/fluentassertions/pull/1782) +* Added overload for `HaveElement` for `XDocument` and `XElement` to assert on number of XML nodes - [#1880](https://github.com/fluentassertions/fluentassertions/pull/1880) ## 6.6.0 diff --git a/docs/_pages/xml.md b/docs/_pages/xml.md index a1447f0c5b..b01a40ba40 100644 --- a/docs/_pages/xml.md +++ b/docs/_pages/xml.md @@ -12,6 +12,8 @@ Fluent Assertions has support for assertions on several of the LINQ-to-XML class ```csharp xDocument.Should().HaveRoot("configuration"); xDocument.Should().HaveElement("settings"); +xDocument.Should().HaveElement("settings", Exactly.Once()); +xDocument.Should().HaveElement("settings", AtLeast.Twice()); xElement.Should().HaveValue("36"); xElement.Should().HaveAttribute("age", "36");