Search code examples
javaxmldiffxmlunitxmlunit-2

XML Unit - Using custom element selectors on different xml elements


I'm having a problem comparing various elements in my xml document with XMLUnit (2.2.1). In my document there are several xml elements and I want to know, whether they differ from each other. However, I don't want to compare all xml elements the same way. Sometimes I just want to compare them by their name.In other cases I want to compare them by name and attribute or name and text.

Here is an example (see the comments)

Control

<?xml version="1.0" encoding="UTF-8"?>
<ROOT>
   <LANGUAGES>
      <!-- Compare LANGUAGE by Name and Attribute -->
      <LANGUAGE VALUE="DE" />
      <LANGUAGE VALUE="EN" />
      <LANGUAGE VALUE="IT" />
      <LANGUAGE VALUE="FR" />
   </LANGUAGES>
   <CODES>
      <!-- Compare CODE by Name and Text -->
      <CODE>10000-1</CODE>
      <CODE>20000-2</CODE>
      <CODE>30000-3</CODE>
      <CODE>40000-4</CODE>
   </CODES>
   <CONTACT> <!-- Compare CONTACT and Children just by Name -->
      <FIRSTNAME>Max</FIRSTNAME>
      <SURNAME>Mustermann</SURNAME>
   </CONTACT>
</ROOT>

Test:

<?xml version="1.0" encoding="UTF-8"?>
<ROOT>
   <LANGUAGES>
      <!-- Compare LANGUAGE by Name and Attribute -->
      <LANGUAGE VALUE="DE" />
      <LANGUAGE VALUE="FR" />
   </LANGUAGES>
   <CODES>
      <!-- Compare CODE by Name and Text -->
      <CODE>20000-2</CODE>
      <CODE>40000-4</CODE>
   </CODES>
   <CONTACT> <!-- Compare CONTACT and Children by Name -->
      <FIRSTNAME>Tim</FIRSTNAME>
      <SURNAME>Mustermann</SURNAME>
   </CONTACT>
</ROOT>

I tried ElementSelectors in combination with ElementSelectors.conditionalBuilder (https://github.com/xmlunit/user-guide/wiki/SelectingNodes#conditional-elementselectors) to only apply an ElementSelector on a specific element (whenElementIsNamed). Maybe this isn't the right approach for what I want to achieve.

This is my code I use for testing:

public void xmlDiff() {
    String control = getControlDocument(); //
    String test = getTestDocument(); //
    Diff myDiff = DiffBuilder.compare(control)//
            .withTest(test) //
            .ignoreWhitespace() //
            .ignoreComments() //
            .checkForSimilar() //
            .withNodeMatcher(new DefaultNodeMatcher(partialElementSelector("LANGUAGE", ElementSelectors.byNameAndAllAttributes), partialElementSelector("CODE",ElementSelectors.byNameAndText), ElementSelectors.byName)) //
            .build();
    assertThat(myDiff.hasDifferences()).isTrue(); // 
}

    private ElementSelector partialElementSelector(final String expectedName, final ElementSelector elementSelector) {
        return ElementSelectors.conditionalBuilder().whenElementIsNamed(expectedName).thenUse(elementSelector).build();
    }

What I actually need is the information, that two LANGUAGEs (EN,IT) and two CODEs (10000-1,30000-3) have been removed (not replaced) and FIRSTNAME changed.

How can I get those information, with or without XML Unit (DiffBuilder)?

Thank you for your help!


Solution

  • There is a subtle difference between using three ElementSelectors in the constructor of DefaultNodeMatcher and using a single ElementSelector with multiple conditions.

    What you want to achieve with the three selectors could also be written as

    ElementSelectors.conditionalBuilder()
        .whenElementIsNamed("LANGUAGE")
        .thenUse(ElementSelectors.byNameAndAllAttributes)
        .whenElementIsNamed("CODE")
        .thenUse(ElementSelectors.byNameAndText)
        .elseUse(ElementSelectors.byName)
        .build();
    

    At first glance this seems to do the same, but it doesn't. In fact it will work. Here is what I get from your example:

    Expected child nodelist length '4' but was '2' - comparing <LANGUAGES...> at /ROOT[1]/LANGUAGES[1] to <LANGUAGES...> at /ROOT[1]/LANGUAGES[1] (DIFFERENT)
    Expected child 'LANGUAGE' but was 'null' - comparing <LANGUAGE...> at /ROOT[1]/LANGUAGES[1]/LANGUAGE[2] to <NULL> (DIFFERENT)
    Expected child 'LANGUAGE' but was 'null' - comparing <LANGUAGE...> at /ROOT[1]/LANGUAGES[1]/LANGUAGE[3] to <NULL> (DIFFERENT)
    Expected child nodelist length '4' but was '2' - comparing <CODES...> at /ROOT[1]/CODES[1] to <CODES...> at /ROOT[1]/CODES[1] (DIFFERENT)
    Expected child 'CODE' but was 'null' - comparing <CODE...> at /ROOT[1]/CODES[1]/CODE[1] to <NULL> (DIFFERENT)
    Expected child 'CODE' but was 'null' - comparing <CODE...> at /ROOT[1]/CODES[1]/CODE[3] to <NULL> (DIFFERENT)
    Expected text value 'Max' but was 'Tim' - comparing <FIRSTNAME ...>Max</FIRSTNAME> at /ROOT[1]/CONTACT[1]/FIRSTNAME[1]/text()[1] to <FIRSTNAME ...>Tim</FIRSTNAME> at /ROOT[1]/CONTACT[1]/FIRSTNAME[1]/text()[1] (DIFFERENT)
    

    So here is what happens: When XMLUnit looks at the LANGUAGE with VALUE en the first conditional ElementSelector you've built will return false for all candidate LANGUAGE elements.

    When using the multi-arg DefaultNodeMatcher constructor the next ElementSelector will be asked - which will also return false in your case as it is only interested in CODE elements. And then the third ElementSelector is consulted which will happily accept any test element named LANGUAGE.

    When using the single ElementSelector I've set up above the combined selector will never consult any of the alternatives as soon as a Predicate has returned true. This means there will be no second thoughts for the LANGUAGE element once the selector has returned false.

    I'll try to update https://github.com/xmlunit/user-guide/wiki/SelectingNodes#nodematcher with your example and would love to get your help with improving the documentation.