34

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);
6
  • 3
    parse the xml and add the values in a set Commented May 14, 2013 at 10:06
  • Any way to do it in just a couple of lines in XMLUnit? Commented May 14, 2013 at 10:06
  • Obviously you shoud just write it as you need for your case. There is no ready solution, because it is unusual requirement. Commented May 14, 2013 at 10:06
  • 1
    Parse XML and make list of text node an compare with other List. Commented May 14, 2013 at 10:08
  • 1
    maybe it is easier to get all the values for xpath (XPathExpression) for building that set Commented May 14, 2013 at 10:12

9 Answers 9

30

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...

Sign up to request clarification or add additional context in comments.

3 Comments

Yes, it helped, and it is a perfect, pure solution.
It is DiffBuilder.compare... instead of Diffbuilder.compare... in 2.4.0 version.
@Akzidenzgrotesk This solution is not working for me, and I tried several other ways as well. stackoverflow.com/questions/74848577/… Any clue where am I missing ?
19

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()));
}

6 Comments

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);
this is ok, just change the set to list, i didnt say it's a set
you said ignoring element order :D
+1: anyway, loading XML then run some XPath on it is definitively not straight forward in Java...
@LiviuStirb : I tried your solution but it doesn't seem to work for me. My XMLs can be seen at stackoverflow.com/questions/74848577/… Any idea what am I missing?
|
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

1 Comment

In my case XMLUnit 1.6 requires to pass false to second argument in order to achive similar comparison instead of identical.
2

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();
}

1 Comment

I totally agree with you on the FQN. I couldn't find the 'isSimilarTo()' anywhere. BTW your solution works for me.
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

Comments

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);

Comments

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()

Comments

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.

1 Comment

@option2 : any way to do it in an automated way?
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));
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.