Search code examples
javaxmlxmlunitxmlunit-2

xmlunit: Error with ElementSelectors.conditionalBuilder


So I have the following input, expected output and actual output xml:

input.xml

<Request>
  <EmailSubjectLine>Main Contact &amp; No Reported To</EmailSubjectLine>
  <ProductRq>
      <Signon>
        <ClientDt>1/6/2017 11:25:45 AM</ClientDt>
        <CustLangPref>en-US</CustLangPref>
      </Signon>
      <SvcRq>
          <RqUID>xxxxxxxx-2802-xxxx-xxxx-bf8361xxxxxx</RqUID>
          <NotificationRq>
              <TransactionRequestDt>2017-01-06</TransactionRequestDt>
              <Currency>USD</Currency>
          </NotificationRq>
      </SvcRq>
  </ProductRq>
<!-- rest of input -->
</Request>

expected-output.xml

<ProductRq xmlns="http://test.org/standards/intake">
    <Audit>
        <TransID>Test</TransID>
    </Audit>
    <Signon>
        <ClientDt>1/6/2017 11:25:45 AM</ClientDt>
        <CustLangPref>en-US</CustLangPref>
    </Signon>
    <SvcRq>
        <RqUID>xxxxxxxx-2802-xxxx-xxxx-bf8361xxxxxx</RqUID>
        <NotificationRq>
            <RqUID>Test</RqUID>
            <TransactionRequestDt>2017-01-06</TransactionRequestDt>
            <Currency>USD</Currency>
        </NotificationRq>
    </SvcRq>
    <!-- rest of expected-output -->
</ProductRq>

actual-output.xml

<ProductRq xmlns="http://test.org/standards/intake">
    <Audit>
        <TransID>123534Abwe-asdcv-1258qw-asd</TransID>
    </Audit>
    <Signon>
        <ClientDt>1/6/2017 11:25:45 AM</ClientDt>
        <CustLangPref>en-US</CustLangPref>
    </Signon>
    <SvcRq>
        <RqUID>xxxxxxxx-2802-xxxx-xxxx-bf8361xxxxxx</RqUID>
        <NotificationRq>
            <RqUID>CG-17Dawe-12354-Hw35Sf</RqUID>
            <TransactionRequestDt>2017-01-06</TransactionRequestDt>
            <Currency>USD</Currency>
        </NotificationRq>
    </SvcRq>
    <!-- rest of actual-output -->
</ProductRq>

I'm comparing them with the following Diff set up:

MyTest.java

        Diff diff = DiffBuilder
                .compare(xmlExpectedOutput)
                .withTest(xmlOutput)
                .normalizeWhitespace()
                .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.conditionalBuilder()
                        .whenElementIsNamed("Audit")
                        .thenUse(ElementSelectors.byXPath("./TransID", ElementSelectors.byName))
                        .whenElementIsNamed("NotificationRq")
                        .thenUse(ElementSelectors.byXPath("./RqUID", ElementSelectors.byName))
                        .elseUse(ElementSelectors.byNameAndText)
                        .build()
                        ))
                .checkForSimilar()
                .build();

I get the following differences when I run the above input and compare with expected-output.xml:

[Expected child '{http://test.org/standards/intake}RqUID' but was 'null' - comparing <RqUID...> at /ProductRq[1]/SvcRq[1]/NotificationRq[1]/RqUID[1] to <NULL> (DIFFERENT), Expected child 'null' but was '{http://test.org/standards/intake}RqUID' - comparing <NULL> to <RqUID...> at /ProductRq[1]/SvcRq[1]/NotificationRq[1]/RqUID[1] (DIFFERENT)]

I don't get why my Element selector wouldn't work, am I using it incorrectly? My aim is whenever TransmissionId or NotificationRq/RqUID are found, to match them with the expected output versions by name only, otherwise use name and text for other elements as these elements contain unique generated ids that change every test run and can't be predicted(with a view to creating a more complex selector later, e.g. to compare ProductRq via name and attribute as a namespace is added to this). Is there something I'm missing, and am I able to combine the 2 XPath selectors together rather than several when/then lines and the default case?

Note: the xml is transformed via xslt. The namespace on PRoductRq is not there on the source document; the source is copied, the namespace added to ProductRq and then sent for output along with some element removals/modifications/additions


Solution

  • XMLUnit says the RqUID elements inside the NotificationRq wouldn't match and of course they are different.

    .whenElementIsNamed("NotificationRq")
    .thenUse(ElementSelectors.byXPath("./RqUID", ElementSelectors.byName))
    

    means: when XMLUnit tries to find a partner for an NotificationRq element then it has to search for an NotificationRq that has an RqUID child - and only use the RqUID element.

    It doesn't set up any rules for any other element, in particular RqUID itself. For RqUID elements the default rules apply and

    .elseUse(ElementSelectors.byNameAndText)
    

    says: XMLUnit only accepts two elements as pairs if their names and the nested text match. Which is not the case for the RqUID elements in question.

    Your whole ElementSelector says

    • match Audits if they have TransID children of arbitrary content.
    • match NotificationRqs if they have RqUID of arbitrary content.
    • use element name and nested text otherwise

    which doesn't fit your example. Looking at your XML you probably wanted

    • match almost everything by element name and nested text (although from the example the element name would be enough)
    • ignore the nested text of TransId children of Audits
    • ignore the nested text of RqUID children of NotificationRq

    There is no built-in predicate for "element named foo if it is a child of an element named bar", it could be something like

    Predicate<Element> transIdInAudit = e -> {
         if (e == null || e.getParentNode() == null) {
             return false;
         }
         return "TransID".equals(e.getLocalName()) && "Audit".equals(e.getParentNode().getLocalName());
    };
    

    which you likely want to make generalizable :-)

    With that you'd use

    .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.conditionalBuilder()
        .when(transIdInAudit)
        .thenUse(ElementSelectors.byName)
        .when(rqUIDInNotificationRq) // similar to transIdInAudit
        .thenUse(ElementSelectors.byName)
        .elseUse(ElementSelectors.byNameAndText)
        .build())
    

    Maybe you really want to match SvcRq if they have matching RqUID, maybe not. If so you'd use the structure you currently use for NotificationRq.

    This in itself will not be enough to ignore the nested text of the matched TransId and RqUID elements, it will only ensure XMLUnit will pick the nodes you want it to use. For the nested text you'll need a DifferenceEvaluator.

    Given that you are using ElementSelectors.byNameAndText by default, you know the nested texts are the same for all matched nodes except for the two specific elements where you want to ignore the content. So a DifferenceEvaluator like

    DifferenceEvaluators.chain(DifferenceEvaluators.Default,
        DifferenceEvaluators.downgradeDifferencesToEqual(ComparisonType.TEXT_VALUE))
    

    should work.