XmlAssert: AssertJ Assertions for XMLUnit With Examples

Learn XmlAssert, the AssertJ API of XMLUnit, to check XML in Java tests by XPath, compare documents, ignore order and whitespace, and validate XSD.

XmlAssert.assertThat() returns XmlAssert, which leads to MultipleNodeAssert, ValueAssert and CompareAssert

XmlAssert is the AssertJ entry point of XMLUnit 2, and it lets us check XML documents in tests with fluent assertions such as hasXPath(), valueByXPath() and and(…).areSimilar(). It comes in the xmlunit-assertj3 module and works on top of XMLUnit, so it accepts the same XML sources and comparison options.

We use XmlAssert to test code that produces XML, such as a SOAP response, an export file, a sitemap or a REST endpoint that returns application/xml. One assertion can check a single value by XPath or compare the whole document with an expected one.

The following example checks a small book document in three ways.

String xml = "<book id=\"b1\"><title>Dune</title><year>1965</year></book>";
String reordered = "<book id=\"b1\"><year>1965</year><title>Dune</title></book>";

assertThat(xml).hasXPath("/book/title");                              // passes
assertThat(xml).valueByXPath("/book/year").asInt().isEqualTo(1965);   // passes
assertThat(xml).and(reordered).ignoreChildNodesOrder().areSimilar();  // passes

Notice that the third assertion passes only because we allow a different child order. In the next sections, we add the dependency, check nodes, values and whole documents, read the failure messages and write a complete test for an XML export.

1. XmlAssert Maven Dependency

Add xmlunit-assertj3 to the test scope. It brings xmlunit-core with it and needs AssertJ 3.18.1 or later; since XMLUnit 2.12.0 it depends on AssertJ 3.27.7. The examples use XMLUnit 2.14.0 on Java 25.

<dependency>
    <groupId>org.xmlunit</groupId>
    <artifactId>xmlunit-assertj3</artifactId>
    <version>2.14.0</version>
    <scope>test</scope>
</dependency>

The class from xmlunit-assertj3 is org.xmlunit.assertj3.XmlAssert, so the static import must use the assertj3 package. The package org.xmlunit.assertj belongs to the older xmlunit-assertj module, which the XMLUnit release notes keep for AssertJ 2.x users.

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

If the test class also uses the regular AssertJ assertThat(), we import one of the two by class name, for example XmlAssert.assertThat(xml), because with both static imports, a String argument picks AssertJ’s assertThat(String), which has no XML methods, and an Object argument makes the call ambiguous.

2. XML Sources That XmlAssert Accepts

The method assertThat() takes an Object and hands it to XMLUnit’s Input class. So we can pass any XML source that XMLUnit understands without converting it first.

  • A String with the XML text, or a byte[] with the encoded text.
  • A File, a Path, an InputStream or a Reader.
  • A DOM Document or Node, and a javax.xml.transform.Source.
  • A JAXB object annotated with @XmlRootElement, when a JAXB implementation is on the classpath.
  • An Input.Builder, such as Input.fromFile(“catalog.xml”), when we want to decorate the source first.

For our examples, a small helper returns the catalog XML that an export endpoint produces. In a real test, this would be the response body or the file the code wrote.

static String catalogXml() {
    return """
        <catalog>
          <book id="b1" lang="en">
            <title>Dune</title>
            <year>1965</year>
            <price>9.99</price>
          </book>
          <book id="b2" lang="de">
            <title>Momo</title>
            <year>1973</year>
            <price>7.50</price>
          </book>
        </catalog>
        """;
}
String xml = catalogXml();
Path file = Files.writeString(Files.createTempFile("catalog", ".xml"), xml);

assertThat(xml).hasXPath("/catalog/book");                         // String
assertThat(file.toFile()).hasXPath("/catalog/book");               // File
assertThat(xml.getBytes(StandardCharsets.UTF_8)).hasXPath("//title");  // byte[]
assertThat(Input.fromPath(file)).hasXPath("//price");              // Input.Builder

3. How the assertThat() Chain Works

The static method XmlAssert.assertThat() wraps the XML in an XmlAssert object, and each method we call on it returns a more specific assertion object. The return type decides what we can check next, so the IDE completion shows only the methods that make sense for nodes, for values or for a comparison.

XmlAssert.assertThat() returns XmlAssert, which leads to MultipleNodeAssert, ValueAssert and CompareAssert
Each XmlAssert method returns a narrower assertion type for nodes, values or whole-document comparison
Method on XmlAssertReturnsUsed for
hasXPath(xpath), nodesByXPath(xpath)MultipleNodeAssertChecking that nodes exist and checking their attributes and children
doesNotHaveXPath(xpath)voidChecking that no node matches
valueByXPath(xpath)ValueAssertChecking the text result of an XPath expression as a String, number or boolean
and(control)CompareAssertComparing the whole document with an expected document
isValid(), isValidAgainst(schema)XmlAssertValidating the document against an XML Schema
withNamespaceContext(map)XmlAssertBinding prefixes for XPath expressions

Since every type extends an AssertJ class, the usual AssertJ methods such as as(“description”), hasSize() or startsWith() work on them too. The AssertJ documentation lists those methods, and our AssertJ tutorial covers the basics.

4. XML Assertion Examples

All examples run against the catalog from section 2. Each line either passes or throws an AssertionError that names the XPath or the difference.

4.1. Checking If a Node Exists

The method hasXPath() passes when at least one node matches the expression, and doesNotHaveXPath() passes when none does. Both return early with a clear message, so they are the simplest way to check the shape of a document.

String xml = catalogXml();

assertThat(xml).hasXPath("/catalog/book[@id='b2']/title");         // passes
assertThat(xml).doesNotHaveXPath("/catalog/book/isbn");            // passes
assertThat(xml).nodesByXPath("/catalog/book").hasSize(2);          // passes
assertThat(xml).nodesByXPath("//book").haveAttribute("lang");      // passes
assertThat(xml).nodesByXPath("//book").containsAllNodesHavingXPath("price");  // passes

The method nodesByXPath() returns all matching nodes as a MultipleNodeAssert. It extends the AssertJ iterable assertions, so hasSize() and element(0) work, and it adds XML methods such as haveAttribute(), doNotHaveAttribute(), containsAnyNodeHavingXPath() and containsAllNodesHavingXPath().

To check the text or the attribute values of all matched nodes at once, we extract them into a list and use the normal AssertJ list assertions.

String xml = catalogXml();

assertThat(xml).nodesByXPath("//book/title").extractingText().containsExactly("Dune", "Momo");  // passes
assertThat(xml).nodesByXPath("//book").extractingAttribute("lang").containsOnly("en", "de");     // passes

4.2. Testing Values

The method valueByXPath() evaluates the expression and returns the result as a ValueAssert, which is an AssertJ String assertion. For numbers and booleans, we convert the value first with asInt(), asDouble() or asBoolean(), so the comparison is numeric and not textual.

String xml = catalogXml();

assertThat(xml).valueByXPath("//book[@id='b1']/title").isEqualTo("Dune");          // passes
assertThat(xml).valueByXPath("count(//book)").asInt().isEqualTo(2);                  // passes
assertThat(xml).valueByXPath("sum(//price)").asDouble().isCloseTo(17.49, within(0.001));  // passes
assertThat(xml).valueByXPath("//book[@id='b2']/year > 1970").asBoolean().isTrue();   // passes
assertThat(xml).valueByXPath("//book[@id='b2']/@lang").isEqualTo("de");             // passes

XPath functions such as count(), sum() and normalize-space() work inside the expression, because XMLUnit uses the JDK XPath engine. Our XPath tutorial explains the expression syntax in more detail.

A ValueAssert has all AssertJ String methods. These are the ones we use most for XML values.

CheckMethods
Empty or blank textisEmpty(), isNotEmpty(), isBlank(), isNotBlank()
EqualityisEqualTo(), isEqualToIgnoringCase(), isEqualToIgnoringWhitespace(), isEqualToNormalizingWhitespace()
Parts of the textcontains(), startsWith(), endsWith(), containsOnlyDigits()
Patternsmatches(“\\d{4}”), doesNotMatch()
Numbers and flagsasInt(), asDouble(), asBoolean(), followed by the AssertJ number or boolean methods
Nested XMLasXml() parses the value as XML and returns a new XmlAssert

4.3. Validating Identical and Similar XMLs

The method and() takes the expected document, which XMLUnit calls the control, and returns a CompareAssert. The two documents are identical when they have the same nodes in the same order with the same values. They are similar when the only differences are ones that do not change the meaning, such as a CDATA section instead of plain text or a different namespace prefix for the same namespace URI.

A different order of child elements is not similar by default, because the default node matcher pairs the children by position. We either allow any order with ignoreChildNodesOrder() or pick a matcher such as ElementSelectors.byName, which pairs elements with the same name wherever they are.

String xml = "<book><title>Dune</title><year>1965</year></book>";
String same = "<book><title>Dune</title><year>1965</year></book>";
String reordered = "<book><year>1965</year><title>Dune</title></book>";
String other = "<book><title>Momo</title><year>1973</year></book>";

assertThat(xml).and(same).areIdentical();                         // passes
assertThat(xml).and(reordered).areNotIdentical();                 // passes
assertThat(xml).and(reordered).areNotSimilar();                   // passes, order matters by default
assertThat(xml).and(reordered).ignoreChildNodesOrder().areSimilar();  // passes
assertThat(xml).and(reordered).withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName)).areSimilar();  // passes
assertThat(xml).and(other).areNotSimilar();                       // passes

Pretty-printed XML and comments are other common sources of false failures. The comparison methods of CompareAssert are the same as the options of XMLUnit’s DiffBuilder.

String pretty = """
    <book>
      <!-- generated -->
      <title> Dune </title>
    </book>
    """;
String compact = "<book><title>Dune</title></book>";

assertThat(pretty).and(compact).ignoreWhitespace().ignoreComments().areIdentical();  // passes
assertThat(pretty).and(compact).ignoreComments().areNotIdentical();                  // passes

The method ignoreWhitespace() removes whitespace-only text nodes and trims the rest, normalizeWhitespace() also collapses inner spaces, and ignoreElementContentWhitespace() removes only the whitespace between elements. With withNodeFilter() and withAttributeFilter(), we can leave out parts of the document, for example a generatedAt attribute that changes on every run.

String actual = "<export generatedAt=\"10:15\"><count>2</count></export>";
String expected = "<export generatedAt=\"09:00\"><count>2</count></export>";

assertThat(actual).and(expected).withAttributeFilter(attr -> !attr.getName().equals("generatedAt")).areIdentical();  // passes

4.4. Validating Against an XML Schema

The method isValidAgainst() validates the document against one or more XSD sources and fails with the parser’s error messages. The method isValid() uses the schema locations that the document itself declares with xsi:schemaLocation. A document without such a reference fails isValid(), because there is no schema to validate against.

String xsd = """
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
      <xs:element name="book">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="title" type="xs:string"/>
            <xs:element name="year" type="xs:int"/>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:schema>
    """;
String valid = "<book><title>Dune</title><year>1965</year></book>";
String invalid = "<book><title>Dune</title><year>soon</year></book>";

assertThat(valid).isValidAgainst(Input.fromString(xsd).build());     // passes
assertThat(invalid).isNotValidAgainst(Input.fromString(xsd).build());  // passes

We can also pass a File or a ready javax.xml.validation.Schema object. For schema validation outside of tests, see XSD schema validation in Java.

4.5. XPath With Namespaces

Many real documents, such as SOAP messages and Atom feeds, put their elements in a namespace. An XPath expression without a prefix does not match those elements, so we bind a prefix to the namespace URI with withNamespaceContext() before calling the XPath methods.

String feed = "<c:catalog xmlns:c=\"urn:books\"><c:book><c:title>Dune</c:title></c:book></c:catalog>";
Map<String, String> ns = Map.of("b", "urn:books");

assertThat(feed).withNamespaceContext(ns).valueByXPath("/b:catalog/b:book/b:title").isEqualTo("Dune");  // passes
assertThat(feed).doesNotHaveXPath("/catalog/book");               // passes, no namespace

The prefix in the test does not have to match the prefix in the document. Only the namespace URI counts, which is why b finds elements written with c. Our article on XPath namespace resolution shows the same idea with the plain JDK API.

5. Reading XmlAssert Failure Messages

A good failure message saves a debugging session, and XmlAssert messages name the XPath or the exact node that differs. In a JUnit 5 or JUnit 6 project, AssertJ throws org.opentest4j.AssertionFailedError, which extends AssertionError, so IDEs can show a diff of the expected and actual values.

String xml = catalogXml();

assertThat(xml).valueByXPath("//book[@id='b1']/title").isEqualTo("Dune Messiah");  // AssertionFailedError: expected "Dune Messiah" but was "Dune"
assertThat("<book><title>Dune</title></book>").and("<book><title>Momo</title></book>").areIdentical();  // AssertionFailedError: Expected text value Momo but was Dune
org.opentest4j.AssertionFailedError: [XPath "//book[@id='b1']/title" evaluated to value]
expected: "Dune Messiah"
 but was: "Dune"

org.opentest4j.AssertionFailedError:
Expecting:
 <control instance> and <test instance> to be identical
Expected text value 'Momo' but was 'Dune' - comparing <title ...>Momo</title> at /book[1]/title[1]/text()[1] to <title ...>Dune</title> at /book[1]/title[1]/text()[1]
expected:<<title>Momo</title>> but was:<<title>Dune</title>>

Notice that the argument of and() is the expected document, so the message reads “Expected … Momo but was … Dune”. We can add our own context with AssertJ’s as(), for example assertThat(xml).as(“export for store 7”).hasXPath(…), and the description appears in square brackets at the start of the message.

6. Testing a Partner Catalog Export

Say a bookshop app sends its catalog every night to a partner as an XML file. The partner’s importer rejects the whole file when a book has no price, and it uses the lang attribute to pick a shelf. A unit test for the exporter should check those two rules and the number of books, but it should not break when the exporter adds a new optional element.

The following test checks those three rules with targeted XPath assertions instead of a full document comparison. The CatalogExporter class and its Book record stand for the production code.

record Book(String id, String lang, String title, int year, double price) {}
static class CatalogExporter {
    String export(List<Book> books) {
        StringBuilder out = new StringBuilder("<catalog>");
        for (Book b : books) {
            out.append("<book id=\"").append(b.id()).append("\" lang=\"").append(b.lang()).append("\">")
               .append("<title>").append(b.title()).append("</title>")
               .append("<year>").append(b.year()).append("</year>")
               .append("<price>").append(b.price()).append("</price></book>");
        }
        return out.append("</catalog>").toString();
    }
}
List<Book> books = List.of(new Book("b1", "en", "Dune", 1965, 9.99), new Book("b2", "de", "Momo", 1973, 7.5));
String exported = new CatalogExporter().export(books);

assertThat(exported).as("catalog export").nodesByXPath("/catalog/book").hasSize(2);     // passes
assertThat(exported).nodesByXPath("//book").containsAllNodesHavingXPath("price");          // passes
assertThat(exported).nodesByXPath("//book").extractingAttribute("lang").containsOnly("en", "de");  // passes
assertThat(exported).valueByXPath("count(//book[not(price > 0)])").asInt().isZero();        // passes

We can see that the test describes the partner’s contract and nothing else. When the contract is the whole document, for example a fixed SOAP response, we compare with a stored file instead, as in assertThat(exported).and(new File(“expected-catalog.xml”)).ignoreWhitespace().areIdentical().

7. XmlAssert vs Hamcrest Matchers vs XMLUnit 1.x

Four APIs with similar names test XML in Java, and search results mix them up. The choice depends on the assertion library the project already uses, because all of them except the legacy one wrap the same comparison engine.

APIModule and classStyleWhen to use
XmlAssert (AssertJ)xmlunit-assertj3, org.xmlunit.assertj3.XmlAssertassertThat(xml).valueByXPath(…).isEqualTo(…)Projects that use AssertJ, which Spring Boot’s test starter includes
Hamcrest matchersxmlunit-matchers, CompareMatcher, HasXPathMatcherassertThat(xml, isSimilarTo(expected))Projects built on Hamcrest or older JUnit 4 tests
DiffBuilderxmlunit-core, org.xmlunit.builder.DiffBuilderDiff diff = DiffBuilder.compare(a).withTest(b).build()Code that needs the list of differences, also outside tests
XMLUnit 1.x XMLAssertxmlunit-legacy, org.custommonkey.xmlunit.XMLAssertXMLAssert.assertXMLEqual(a, b)Only old JUnit 3 code; migrate to one of the rows above

The legacy XMLAssert class with capital XML is a different class from a different library generation, so its methods do not work with an XmlAssert import. Our XMLUnit tutorial covers the DiffBuilder and Hamcrest styles.

8. XmlAssert FAQs

Most XmlAssert questions come from a wrong import or from element order, so those two are answered first.

8.1. What is the difference between xmlunit-assertj and xmlunit-assertj3?

The module xmlunit-assertj3 is built for AssertJ 3.x and puts XmlAssert in the package org.xmlunit.assertj3. The older xmlunit-assertj module uses the package org.xmlunit.assertj and stays compatible with AssertJ 2.x. New projects use xmlunit-assertj3, as the XMLUnit release notes recommend.

8.2. How do we ignore the element order with XmlAssert?

Call ignoreChildNodesOrder() on the result of and() and finish with areSimilar(), or set a node matcher such as withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName)). For repeated elements with the same name, ElementSelectors.byNameAndText or byNameAndAllAttributes pair the right siblings.

8.3. Why does areSimilar() fail for the same elements in a different order?

The default node matcher compares children by position, so title is compared with year and the element names differ. Different element names are a real difference, not a similar one. Choosing a node matcher, as in section 4.3, fixes the pairing.

8.4. Can XmlAssert compare an XML file with an expected file?

Yes. Pass both as File objects or as Input.fromFile(…) builders, for example assertThat(new File(“actual.xml”)).and(new File(“expected.xml”)).areSimilar(). Add ignoreWhitespace() when one of the files is pretty-printed.

8.5. Does XmlAssert work with JUnit 5 and JUnit 6?

Yes. XmlAssert depends only on AssertJ and XMLUnit, so it works with any test framework. With JUnit 5 or 6 on the classpath, failures are reported as AssertionFailedError, and the IDE shows a comparison of the expected and actual values.

9. Conclusion

The class org.xmlunit.assertj3.XmlAssert brings XMLUnit’s XPath and comparison engine into AssertJ’s fluent style. We check structure with hasXPath() and nodesByXPath(), single values with valueByXPath() and its asInt(), asDouble() and asBoolean() conversions, and whole documents with and(…).areIdentical() or areSimilar().

Element order, whitespace and comments cause most false failures, and CompareAssert has an option for each of them. For a contract with a partner system, a few targeted XPath assertions keep the test stable when the document grows.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.