32

Suppose I have two xml strings

<test>
  <elem>a</elem>
  <elem>b</elem>
</test>

<test>
  <elem>b</elem>
  <elem>a</elem>
</test>

How to write a test that compares those two strings and ignores the element order?

I want the test to be as short as possible, no place for 10-line XML parsing etc. I'm looking for a simple assertion or something similar.

I have this (which doesn't work)

   Diff diff = XMLUnit.compareXML(expectedString, actualString);   
   XMLAssert.assertXMLEqual("meh", diff, true);
Erik
  • 503
  • 1
  • 7
  • 26
Roay Spol
  • 1,188
  • 1
  • 11
  • 17

9 Answers9

29

For xmlunit 2.0 (I was looking for this) it is now done, by using DefaultNodeMatcher

Diff diff = Diffbuilder.compare(Input.fromFile(control))
   .withTest(Input.fromFile(test))
   .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))
   .build()

Hope this helps this helps other people googling...

Cheeso
  • 189,189
  • 101
  • 473
  • 713
Akzidenzgrotesk
  • 544
  • 4
  • 8
  • Yes, it helped, and it is a perfect, pure solution. – peterh Oct 27 '16 at 17:16
  • 2
    It is DiffBuilder.compare... instead of Diffbuilder.compare... in 2.4.0 version. – Balaban Mario Nov 02 '17 at 08:20
  • @Akzidenzgrotesk This solution is not working for me, and I tried several other ways as well. https://stackoverflow.com/questions/74848577/compare-xml-using-xmlunit-when-order-of-attributes-is-different-is-not-working Any clue where am I missing ? – Abhi Dec 19 '22 at 14:50
18

XMLUnit will do what you want, but you have to specify the elementQualifier. With no elementQualifier specified it will only compare the nodes in the same position.

For your example you want an ElementNameAndTextQualifer, this considers a node similar if one exists that matches the element name and it's text value, something like :

Diff diff = new Diff(control, toTest);
// we don't care about ordering
diff.overrideElementQualifier(new ElementNameAndTextQualifier());
XMLAssert.assertXMLEqual(diff, true);

You can read more about it here: http://xmlunit.sourceforge.net/userguide/html/ar01s03.html#ElementQualifier

Steve Lancashire
  • 1,033
  • 9
  • 5
  • In my case XMLUnit 1.6 requires to pass `false` to second argument in order to achive similar comparison instead of identical. – Vadzim Jun 29 '17 at 17:13
17

My original answer is outdated. If I would have to build it again i would use xmlunit 2 and xmlunit-matchers. Please note that for xml unit a different order is always 'similar' not equals.

@Test
public void testXmlUnit() {
    String myControlXML = "<test><elem>a</elem><elem>b</elem></test>";
    String expected = "<test><elem>b</elem><elem>a</elem></test>";
    assertThat(myControlXML, isSimilarTo(expected)
            .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
    //In case you wan't to ignore whitespaces add ignoreWhitespace().normalizeWhitespace()
    assertThat(myControlXML, isSimilarTo(expected)
        .ignoreWhitespace()
        .normalizeWhitespace()
        .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
}  

If somebody still want't to use a pure java implementation here it is. This implementation extracts the content from xml and compares the list ignoring order.

public static Document loadXMLFromString(String xml) throws Exception {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder = factory.newDocumentBuilder();
    InputSource is = new InputSource(new StringReader(xml));
    return builder.parse(is);
}

@Test
public void test() throws Exception {
    Document doc = loadXMLFromString("<test>\n" +
            "  <elem>b</elem>\n" +
            "  <elem>a</elem>\n" +
            "</test>");
    XPathFactory xPathfactory = XPathFactory.newInstance();
    XPath xpath = xPathfactory.newXPath();
    XPathExpression expr = xpath.compile("//test//elem");
    NodeList all = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
    List<String> values = new ArrayList<>();
    if (all != null && all.getLength() > 0) {
        for (int i = 0; i < all.getLength(); i++) {
            values.add(all.item(i).getTextContent());
        }
    }
    Set<String> expected = new HashSet<>(Arrays.asList("a", "b"));
    assertThat("List equality without order",
            values, containsInAnyOrder(expected.toArray()));
}
Adrian Leonhard
  • 7,040
  • 2
  • 24
  • 38
Liviu Stirb
  • 5,876
  • 3
  • 35
  • 40
  • the xml dom; i load it using:DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); InputSource is = new InputSource(new StringReader(xml)); return builder.parse(is); – Liviu Stirb May 14 '13 at 10:24
  • this is ok, just change the set to list, i didnt say it's a set – Roay Spol May 14 '13 at 10:28
  • you said ignoring element order :D – Liviu Stirb May 14 '13 at 10:37
  • +1: anyway, loading XML then run some XPath on it is **definitively** not straight forward in Java... – Stephan May 14 '13 at 12:14
  • @LiviuStirb : I tried your solution but it doesn't seem to work for me. My XMLs can be seen at https://stackoverflow.com/questions/74848577/compare-xml-using-xmlunit-when-order-of-attributes-is-different-is-not-working Any idea what am I missing? – Abhi Dec 19 '22 at 13:47
1

Cross-posting from Compare XML ignoring order of child elements

I had a similar need this evening, and couldn't find something that fit my requirements.

My workaround was to sort the two XML files I wanted to diff, sorting alphabetically by the element name. Once they were both in a consistent order, I could diff the two sorted files using a regular visual diff tool.

If this approach sounds useful to anyone else, I've shared the python script I wrote to do the sorting at http://dalelane.co.uk/blog/?p=3225

Community
  • 1
  • 1
dalelane
  • 2,746
  • 1
  • 24
  • 27
1

Just as an example of how to compare more complex xml elements matching based on equality of attribute name. For instance:

<request>
     <param name="foo" style="" type="xs:int"/>
     <param name="Cookie" path="cookie" style="header" type="xs:string" />
</request>

vs.

<request>
     <param name="Cookie" path="cookie" style="header" type="xs:string" />
     <param name="foo" style="query" type="xs:int"/>
</request>

With following custom element qualifier:

final Diff diff = XMLUnit.compareXML(controlXml, testXml);
diff.overrideElementQualifier(new ElementNameAndTextQualifier() {

    @Override
    public boolean qualifyForComparison(final Element control, final Element test) {
        // this condition is copied from super.super class
        if (!(control != null && test != null
                      && equalsNamespace(control, test)
                      && getNonNamespacedNodeName(control).equals(getNonNamespacedNodeName(test)))) {
            return false;
        }

        // matching based on 'name' attribute
        if (control.hasAttribute("name") && test.hasAttribute("name")) {
            if (control.getAttribute("name").equals(test.getAttribute("name"))) {
                return true;
            }
        }
        return false;
    }
});
XMLAssert.assertXMLEqual(diff, true);
mkobit
  • 43,979
  • 12
  • 156
  • 150
Stepan Vavra
  • 3,884
  • 5
  • 29
  • 40
1

For me, I also needed to add the method : checkForSimilar() on the DiffBuilder. Without it, the assert was in error saying that the sequence of the nodes was not the same (the position in the child list was not the same)

My code was :

 Diff diff = Diffbuilder.compare(Input.fromFile(control))
   .withTest(Input.fromFile(test))
   .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))
   .checkForSimilar()
   .build()
Kyle Calica-St
  • 2,629
  • 4
  • 26
  • 56
pierre
  • 11
  • 1
1

I don't know what versions they took for the solutions, but nothing worked (or was simple at least) so here's my solution for who had the same pains.

P.S. I hate people to miss the imports or the FQN class names of static methods

    @Test
void given2XMLS_are_different_elements_sequence_with_whitespaces(){
    String testXml = "<struct><int>3</int>  <boolean>false</boolean> </struct>";
    String expected = "<struct><boolean>false</boolean><int>3</int></struct>";

    XmlAssert.assertThat(testXml).and(expected)
            .ignoreWhitespace()
            .normalizeWhitespace()
            .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))
            .areSimilar();
}
Brendan Kim
  • 295
  • 3
  • 8
  • I totally agree with you on the FQN. I couldn't find the 'isSimilarTo()' anywhere. BTW your solution works for me. – Andre Brito Dec 14 '21 at 11:16
0

OPTION 1
If the XML code is simple, try this:

 String testString = ...
 assertTrue(testString.matches("(?m)^<test>(\\s*<elem>(a|b)</elem>\\s*){2}</test>$"));


OPTION 2
If the XML is more elaborate, load it with an XML parser and compare the actual nodes found with you reference nodes.

Stephan
  • 41,764
  • 65
  • 238
  • 329
0

I end up rewriting the xml and comparing it back. Let me know if it helps any of you who stumbled on this similar issue.

import org.apache.commons.lang3.StringUtils;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;

public class XmlRewriter {
    private static String rewriteXml(String xml) throws Exception {
        SAXBuilder builder = new SAXBuilder();
        Document document = builder.build(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)));
        Element root = document.getRootElement();

        XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat());

        root.sortChildren((o1, o2) -> {
            if(!StringUtils.equals(o1.getName(), o2.getName())){
                return o1.getName().compareTo(o2.getName());
            }
            // get attributes
            int attrCompare = transformToStr(o1.getAttributes()).compareTo(transformToStr(o2.getAttributes()));
            if(attrCompare!=0){
                return attrCompare;
            }
            if(o1.getValue()!=null && o2.getValue()!=null){
                return o1.getValue().compareTo(o2.getValue());
            }
            return 0;
        });
        return xmlOutputter.outputString(root);
    }

    private static String transformToStr(List<Attribute> attributes){
        return attributes.stream().map(e-> e.getName()+":"+e.getValue()).sorted().collect(Collectors.joining(","));
    }

    public static boolean areXmlSimilar(String xml1, String xml2) throws Exception {
        Diff diff = DiffBuilder.compare(rewriteXml(xml1)).withTest(rewriteXml(xml2))
                .normalizeWhitespace()
                .ignoreWhitespace()
                .ignoreComments()
                .checkForSimilar()
                .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText))
                .build();

        return !diff.hasDifferences();
    }

// move below into another test class.. 
    @Test
    public void compareXml() throws Exception {
        String xml1 = "<<your first XML str here>>";
        String xml2 = "<<another XML str here>>";
        assertTrue(XmlUtil.areXmlSimilar(xml1, xml2));
    }
}
fauzimh
  • 594
  • 4
  • 16