简体   繁体   中英

Compare two XML strings ignoring element order

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

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

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

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

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

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

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

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.

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.

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM