1

I need to compare XML files using XMLUnit 2 where xml subnodes can differ in order. Due to the underlying libraries that are used I can't influence order of subnodes.

For comparison I am using:

    <dependency>
        <groupId>org.xmlunit</groupId>
        <artifactId>xmlunit-core</artifactId>
        <version>2.0.0-alpha-02</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.xmlunit</groupId>
        <artifactId>xmlunit-matchers</artifactId>
        <version>2.0.0-alpha-02</version>
        <scope>test</scope>
    </dependency>

The problem boils down to this JUnit test:

import static org.hamcrest.MatcherAssert.assertThat;
import static org.xmlunit.builder.Input.fromString;
import static org.xmlunit.diff.ElementSelectors.byName;
import static org.xmlunit.matchers.CompareMatcher.isSimilarTo;

import org.apache.commons.io.IOUtils;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.xmlunit.diff.DefaultNodeMatcher;

import java.io.IOException;

public class XmlTest {

    @Test
    public void test() throws Exception {
        String t1 = fromClassPath("t1.xml");
        String t2 = fromClassPath("t2.xml");
        assertThat(fromString(t1), isSimilarTo(fromString(t2)).withNodeMatcher(new DefaultNodeMatcher(byName)));
    }

    private static String fromClassPath(String fileName) throws IOException {
        return IOUtils.toString(new ClassPathResource(fileName).getInputStream());
    }
}

where t1.xml contains:

<root>
    <child>
        <node1/>
    </child>
    <child>
        <node2/>
    </child>
</root>

and t2.xml contains:

<root>
    <child>
        <node2/>
    </child>
    <child>
        <node1/>                                                                                                                        
    </child>
</root>

Actually, node1 and node2 are changed in order. The test result is:

java.lang.AssertionError: 
Expected: Expected child 'node2' but was 'null' - comparing <node2...> at /root[1]/child[1]/node2[1] to <NULL>:
<node2/>
     but: result was: 
<NULL>  

I'm wonder if it is possible to compare such xml files with XMLUnit 2. Any kind of help is appreciated.

mkobit
  • 43,979
  • 12
  • 156
  • 150
ksokol
  • 8,035
  • 3
  • 43
  • 56

1 Answers1

5

You tell XMLUnit to match elements by name with no additional conditions. In your case this means the child elements are compared in order. You obviously need a different strategy, one that looks at the first child node of the nodes when deciding which child node to select as "partner" for any given child.

One option would be

    new DefaultNodeMatcher(byXPath("./*[1]", byName), byName))

which would match nodes first by trying to match their first children and - if that fails to bring up any candidate - use the name of the elements themselves.

Depending on the structure of your real documents, this may be too naive and you need to use a conditional element selector

    new DefaultNodeMatcher(selectorForElementNamed("child", byXPath("./*[1]", byName)), byName))

Note: the above code doesn't work with XMLUnit 2.0.0-alpha-02 because of issue #39 - you'd have to use an XPath of "./child/*[1]" instead. The byXPath ElementSelector has been fixed afterwards.

If you don't want to use XPath, you can code an ElementSelector yourself

public static class FirstChildElementNameSelector implements ElementSelector {
    @Override
    public boolean canBeCompared(Element controlElement,
                                 Element testElement) {
        return byName.canBeCompared(firstChildElement(controlElement),
                                    firstChildElement(testElement));
    }

    private Element firstChildElement(Element e) {
        NodeList nl = e.getChildNodes();
        int len = nl.getLength();
        for (int i = 0; i < len; i++) {
            if (nl.item(i) instanceof Element) {
                return (Element) nl.item(i);
            }
        }
        return null;
    }
}

and use it like

    new DefaultNodeMatcher(selectorForElementNamed("child", new FirstChildElementNameSelector()), byName))
Stefan Bodewig
  • 3,260
  • 15
  • 22
  • Thank you for your answer. Unfortunately, it does not work either. I still get the same failure. I debugged into XMLUnit to see what's going on. I tried a different node matcher `or(selectorForElementNamed("node2", byXPath("./../*/node2", byName)), byName)` for `node2` and vice versa for `node1` but still no success. – ksokol Nov 29 '15 at 13:41
  • You are correct @ksokol for two reasons. `byXPath` doesn't work the way I intended it to https://github.com/xmlunit/xmlunit/issues/39 and `or` is not the correct choice. https://github.com/xmlunit/xmlunit/commit/50e4aeab4beebd1e8d31b9409b9b1a57cf434c46 contains two passing tests for you, the one labeled 02 will not work with aplha-03 or 2.0.0 (whatever comes next) as I intend to make the original XPath expression work. I'll edit the answer once I've fixed the XPath issue. – Stefan Bodewig Nov 29 '15 at 18:15
  • Thanks for your elaborated answer. I debugged further through XMLUnit and had the idea of a custom `NodeMatcher`. Your edited answer guided me to the right solution. For the moment I will use `FirstChildElementNameSelector`. As soon as alpha-03 is available in Maven central I will try `byXPath` `ElementSelector`. Thank you! – ksokol Nov 29 '15 at 21:11
  • @ksokol I've published XMLUnit 2.0.0-alpha-03 today. – Stefan Bodewig Dec 13 '15 at 12:15
  • @/stefan-bodewig Thank you. It is working like intended now. – ksokol Dec 14 '15 at 08:52