I have recently faced an issue comparing 2 xmls using org.springframework.test.util.XmlExpectationsHelper#assertXmlEqual(String, String) in unit tests.
According to the source code of org.springframework.test.util.XmlExpectationsHelper
private static class XmlUnitDiff {
private final Diff diff;
XmlUnitDiff(String expected, String actual) {
this.diff = DiffBuilder.compare(expected).withTest(actual)
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))
.ignoreWhitespace().ignoreComments()
.checkForSimilar()
.build();
}
public boolean hasDifferences() {
return this.diff.hasDifferences();
}
@Override
public String toString() {
return this.diff.toString();
}
}
it uses org.xmlunit:xmlunit-core.
Here is example of xmls that lead to error
1st
<Test1 id = "1">
<Test2 test="true" id="1"/>
<Test2 test="false" id="2"/>
</Test1>
2nd
<Test1 id = "1">
<Test2 id="2" test="false"/>
<Test2 id="1" test="true"/>
</Test1>
Error
Expected attribute value '1' but was '2' - comparing <Test2 id="1"...> at /Test1[1]/Test2[1]/@id to <Test2 id="2"...> at /Test1[1]/Test2[1]/@id
org.springframework.test.util.XmlExpectationsHelper uses ElementSelectors.byNameAndText.
xmlunit is looking for matching nodes using ElementSelector#canBeCompared(Element, Element). If ElementSelector#canBeCompared(Element, Element) == true it compares nodes.
As soon as there is no any text content in provided xmls, xmlunit considers that 1st item <Test2 test="true" id="1"/>
can be compared with <Test2 id="2" test="false"/>
.
And an error happens.
So, attributes should also be taken into account. xmlunit has another selector ElementSelectors.byNameAndAllAttributes for this case.
However, changing the code to
DiffBuilder.compare(xml1).withTest(xml2)
.withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndAllAttributes))
.checkForSimilar().ignoreComments()
.ignoreWhitespace()
.build();
will lead to error for such xmls
1st
<Test1>
<Test3>
<Test4>data 1</Test4>
<Test4>data 2</Test4>
</Test3>
<Test2>test data 2</Test2>
</Test1>
2nd
<Test1>
<Test2>test data 2</Test2>
<Test3>
<Test4>data 2</Test4>
<Test4>data 1</Test4>
</Test3>
</Test1>
Error
Expected text value 'data 1' but was 'data 2' - comparing <Test4 ...>data 1</Test4> at /Test1[1]/Test3[1]/Test4[1]/text()[1] to <Test4 ...>data 2</Test4> at /Test1[1]/Test3[1]/Test4[1]/text()[1]
At this time there are no any attributes, and xmlunit tries to compare <Test4>data 1</Test4>
and <Test4>data 2</Test4>
Finally, combining 2 selectors
import org.xmlunit.builder.DiffBuilder;
import org.xmlunit.diff.DefaultNodeMatcher;
import org.xmlunit.diff.ElementSelector;
import org.xmlunit.diff.ElementSelectors;
import java.util.StringJoiner;
public class Main {
public static void compareXml(final String xml1, final String xml2) {
final var diff = DiffBuilder
.compare(xml1)
.withTest(xml2)
.withNodeMatcher(
new DefaultNodeMatcher(
(ElementSelector) (expected, actual) ->
ElementSelectors.byNameAndText.canBeCompared(expected, actual) &&
ElementSelectors.byNameAndAllAttributes.canBeCompared(expected, actual)
)
)
.checkForSimilar()
.ignoreComments()
.ignoreWhitespace()
.build();
if (diff.hasDifferences()) {
throw new AssertionError(
new StringJoiner(System.lineSeparator())
.add(diff.toString())
.add(xml1)
.add(xml2)
.toString()
);
}
}
public static void main(String[] args) {
//org.xmlunit.diff.ElementSelectors.byNameAndText gives error for identical xmls
compareXml(
"""
<Test1 id = "1">
<Test2 test="true" id="1"/>
<Test2 test="false" id="2"/>
</Test1>""",
"""
<Test1 id = "1">
<Test2 id="2" test="false"/>
<Test2 id="1" test="true"/>
</Test1>"""
);
//org.xmlunit.diff.ElementSelectors.byNameAndAllAttributes gives error for identical xmls
compareXml(
"""
<Test1>
<Test3>
<Test4>data 1</Test4>
<Test4>data 2</Test4>
</Test3>
<Test2>test data 2</Test2>
</Test1>""",
"""
<Test1>
<Test2>test data 2</Test2>
<Test3>
<Test4>data 2</Test4>
<Test4>data 1</Test4>
</Test3>
</Test1>"""
);
}
}
This code worked fine for all cases I had. Check nulls as well if nulls are possible.
By the way, if you try
.withNodeMatcher(
new DefaultNodeMatcher(
ElementSelectors.byNameAndText, ElementSelectors.byNameAndAllAttributes
)
)
it will not work as well. You can go deeper in DefaultNodeMatcher to find out what's the point.
I ran all the cases for 2.9.1 version of org.xmlunit:xmlunit-core as well. It's latest now.