Skip to content
Stefan Bodewig edited this page May 1, 2024 · 13 revisions

Obtaining the Results of an XPath Query

The (I)XPathEngine interface inside the xpath/XPath package/namespace provides two pairs of methods that apply an XPath expression to a piece of XML and return the result as a string or collection of DOM nodes.

The first pair expects the "piece of XML" to be provided as (I)Source like most other code in XMLUnit as well, the second pair directly works on DOM nodes. Initially only the Source-arg versions existed, but the underlying implementations can work efficiently on nodes directly, so the overloads have been added.

For Java as well as .NET only a single implementation of the XPathEngine interface exists - they use javax.xml.xpath and System.Xml.XPath under the covers directly. In the Java case all checked exceptions will be transformed to runtime exceptions.

In order to use namespace prefixes inside the XPath expressions you need to provide a NamespaceContext.

Examples:

Selecting nodes:

Iterable<Node> i = new JAXPXPathEngine().selectNodes("//li", source);
assertNotNull(i);
int count = 0;
for (Iterator<Node> it = i.iterator(); it.hasNext(); ) {
    count++;
    assertEquals("li", it.next().getNodeName());
}
assertEquals(4, count);

in Java. For .NET it would be

IEnumerable<XmlNode> i = new XPathEngine().SelectNodes("//li", source);
Assert.IsNotNull(i);
int count = 0;
foreach (XmlNode n in i) {
    count++;
    Assert.AreEqual("li", n.Name);
}
Assert.AreEqual(4, count);

Evaluating an XPath to a string:

assertEquals("Don't blame it on the...",
             new JAXPXPathEngine().evaluate("//title", source));

in Java. For .NET it would be

Assert.AreEqual("Don't blame it on the...",
                new XPathEngine().Evaluate("//title", source));

HasXPathMatcher / HasXPathConstraint

XMLUnit for Java provides a Hamcrest macher named HasXPathMatcher and XMLUnit.NET NUnit (2.x and 3.x) constraints HasXPathConstraint that can be used to verify if a XPath expression corresponds to at least one element in the provided XML input.

The Java Hamcrest matcher can be used in the following manner :

import static org.xmlunit.matchers.HasXPathMatcher.hasXPath;
import static org.hamcrest.core.IsNot.not;

...

final String xml = "<a><b attr=\"abc\"></b></a>";
assertThat(xml, hasXPath("//a/b/@attr"));
assertThat(xml, not(hasXPath("//a/b/c")));
   

The corresponding .NET code would be

string xml = "<a><b attr=\"abc\"></b></a>";
Assert.That(xml, HasXPathConstraint.HasXPath("//a/b/@attr"));
Assert.That(xml, !HasXPathConstraint.HasXPath("//a/b/c"));

The matcher and constraints also provide XML NamespaceContext support using the withNamespaceContext method :

import static org.xmlunit.matchers.HasXPathMatcher.hasXPath;

...

String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
    "<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
    "   <title>title</title>" +
    "   <entry>" +
    "       <title>title1</title>" +
    "       <id>id1</id>" +
    "   </entry>" +
    "</feed>";

HashMap<String, String> prefix2Uri = new HashMap<String, String>();
prefix2Uri.put("atom", "http://www.w3.org/2005/Atom");
assertThat(xmlRootElement,
    hasXPath("//atom:feed/atom:entry/atom:id").withNamespaceContext(prefix2Uri));

EvaluateXPathMatcher / EvaluateXPathConstraint

XMLUnit for Java provides a Hamcrest macher named EvaluateXPathMatcher and XMLUnit.NET NUnit (2.x and 3.x) constraints EvaluateXPathConstraint that
can be used to verify if the evaluation of the provided XPath expression corresponds to the value matcher specified for the provided input XML object.

The Java Hamcrest matcher can be used in the following manner :

import static org.xmlunit.matchers.EvaluateXPathMatcher.hasXPath;
import static org.hamcrest.CoreMatchers.equalTo;

...
  
final String xml = "<a><b attr=\"abc\"></b></a>";  
assertThat(xml, hasXPath("//a/b/@attr", equalTo("abc")));
assertThat(xml, hasXPath("count(//a/b/c)", equalTo("0")));

The corresponding .NET code would be

string xml = "<a><b attr=\"abc\"></b></a>";  
Assert.That(xml, EvaluateXPathConstraint.HasXPath("//a/b/@attr", Is.EqualTo("abc")));
Assert.That(xml, EvaluateXPathConstraint.HasXPath("count(//a/b/c)", Is.EqualTo("0")));

The matcher and constraints also provide XML NamespaceContext support using the withNamespaceContext method :

import static org.xmlunit.matchers.EvaluateXPathMatcher.hasXPath;

String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
    "<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
    "   <title>title</title>" +
    "   <entry>" +
    "       <title>title1</title>" +
    "       <id>id1</id>" +
    "   </entry>" +
    "</feed>";

HashMap<String, String> prefix2Uri = new HashMap<String, String>();
prefix2Uri.put("atom", "http://www.w3.org/2005/Atom");
assertThat(xmlRootElement,
    hasXPath("//atom:feed/atom:entry/atom:id/text()", equalTo("id1"))
    .withNamespaceContext(prefix2Uri));

MultipleNodeAssert / SingleNodeAssert

XMLUnit for Java provides AssertJ assert classes named MultipleNodeAssert and SingleNodeAssert that can be used to verify if a XPath expression corresponds to at least one element in the provided XML input and perform assertions on those elements.

Access to MultipleNodeAssert is provided by XmlAssert.nodesByXPath(XPath).
Using first(), last(), element() on MultipleNodeAssert navigates to corresponding element node and allow to perform assertions provided by SingleNodeAssert.

There are two XMLUnit modules supporting AssertJ. xmlunit-assertj supports AssertJ 2.x and 3.x while xmlunit-assertj3 requires at least AssertJ 3.18.1. The examples here assume xmlunit-assertj, with xmlunit-assertj3 the Java package has to be changed to org.xmlunit.assertj3.

import static org.xmlunit.assertj.XmlAssert.assertThat;

...

final String xml = "<a><b attr=\"abc\"></b></a>"; 
assertThat(xml).nodesByXPath("//a/b/@attr") // MultipleNodeAssert type
               .exist();
assertThat(xml).hasXPath("//a/b/@attr");

assertThat(xml).nodesByXPath("//a/b/c")     // MultipleNodeAssert type
               .doNotExist();
assertThat(xml).doesNotHaveXPath("//a/b/c");
import static org.xmlunit.assertj.XmlAssert.assertThat;

final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
        "   <title>title</title>" +
        "   <entry>" +
        "       <title>title1</title>" +
        "       <id>id1</id>" +
        "   </entry>" +
        "</feed>";


final Map<String, String> prefix2Uri = new HashMap<String, String>();
prefix2Uri.put("atom", "http://www.w3.org/2005/Atom");

assertThat(xml)
        .withNamespaceContext(prefix2Uri)
        .hasXPath("//atom:feed/atom:entry/atom:id");
import static org.xmlunit.assertj.XmlAssert.assertThat;

final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<feed>" +
        "   <title>title</title>" +
        "   <entry attr=\"abc\" attr2=\"value\" foo=\"bar\">" +
        "       <title>title1</title>" +
        "   </entry>" +
        "   <entry attr=\"xyz\" attr2=\"value\">" +
        "       <title>title1</title>" +
        "   </entry>" +
        "</feed>";

assertThat(xml).nodesByXPath("/feed/entry")
               .haveAttribute("attr")
               .haveAttribute("attr2", "value");

assertThat(xml).nodesByXPath("/feed/entry")    // MultipleNodeAssert type
               .first()                        // SingleNodeAssert type
               .hasAttribute("foo");

assertThat(xml).nodesByXPath("/feed/entry")    // MultipleNodeAssert type
               .last()                         // SingleNodeAssert type
               .doesNotHaveAttribute("foo");

assertThat(xml).nodesByXPath("/feed/entry")    // MultipleNodeAssert type
               .element(1)                     // SingleNodeAssert type
               .doesNotHaveAttribute("foo");

ValueAssert

XMLUnit for Java 7+ provides an AssertJ assert class named ValueAssert that can be used to verify if the evaluation of the provided XPath expression meets certain conditions.

Access to ValueAssert is provided by XmlAssert.valueByXPath(XPath).

import static org.xmlunit.assertj.XmlAssert.assertThat;

final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<fruits>" +
        "   <fruit name=\"apple\"/>" +
        "   <fruit name=\"orange\"/>" +
        "   <fruit name=\"banana\"/>" +
        "</fruits>";

assertThat(xml).valueByXPath("count(//fruits/fruit)").isEqualTo("3");                             
assertThat(xml).valueByXPath("count(//fruits/fruit)").isEqualTo(3);
assertThat(xml).valueByXPath("count(//fruits/fruit[@name=\"orange\"])").isEqualTo(1);
assertThat(xml).valueByXPath("count(//fruits/fruit[@name=\"apricot\"])").isEqualTo(0);

assertThat(xml).valueByXPath("count(//fruits/fruit)")  // ValueAssert type
               .asInt()           // org.assertj.core.api.AbstractIntegerAssert type
               .isEqualTo(3)
               .isGreaterThan(2);
final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<fruits>" +
        "   <fruit name=\"apple\" weight=\"66.6\"/>" +
        "</fruits>";

assertThat(xml).valueByXPath("//fruits/fruit/@weight")  // ValueAssert type
               .asDouble()         // org.assertj.core.api.AbstractDoubleAssert type
               .isEqualTo(66.6)
               .isPositive();
final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<fruits>" +
        "   <fruit name=\"apple\" fresh=\"True\"/>" +
        "   <fruit name=\"pear\" fresh=\"0\"/>" +
        "</fruits>";

assertThat(xml).valueByXPath("//fruits/fruit[@name=\"apple\"]/@fresh")  // ValueAssert type
               .asBoolean()                        // org.assertj.core.api.AbstractBooleanAssert type
               .isTrue();

assertThat(xml).valueByXPath("//fruits/fruit[@name=\"pear\"]/@fresh").asBoolean().isFalse();
final String xml ="<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<a><b>" +
        "<![CDATA[<c><d attr=\"xyz\"></d></c>]]>" +
        "</b></a>";

assertThat(xml).valueByXPath("//a/b/text()")        // ValueAssert type
        .isEqualTo("<c><d attr=\"xyz\"></d></c>")
        .asXml()                                    // XmlAssert type
        .hasXPath("/c/d");
import static org.xmlunit.assertj.XmlAssert.assertThat;

final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
        "<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
        "   <title>title</title>" +
        "   <entry>" +
        "       <title>title1</title>" +
        "       <id>id1</id>" +
        "   </entry>" +
        "</feed>";


final Map<String, String> prefix2Uri = new HashMap<String, String>();
prefix2Uri.put("atom", "http://www.w3.org/2005/Atom");

assertThat(xml)
        .withNamespaceContext(prefix2Uri)
        .valueByXPath("//atom:feed/atom:entry/atom:id/text()")
        .isEqualTo("id1");

Extension Functions

Some XPath implementations allow the use of extension functions to add new functions that can be used in XPath expressions. These functions can invoke user provided code.

XMLUnit for Java

JAXP's XPathFactory by default allows the execution of extension functions and so did the default XPath used internally prior to XMLUnit for Java 2.10.0. Starting with 2.10.0 the default has been changed to disable extension functions - at least when using Java 18 or later.

This means if you want to protect yourself against extension functions and you use a version of XMLUnit prior to 2.10.0 you have to explicitly set an XPathFactory that is configured properly. Likewise if you rely on extension functions you must provide an explicit XPathFactory when using XMLUnit 2.10.0 or later.

Please see JAXP Security Guide for details.

Prior to Java 18 there is no way to configure properties for an XPathFactory instance, here you may need to set global system properties or enable the secure processing feature on the factory.

XMLUnit.NET

´XPathNavigator` doesn't support extension functions at all.