Einführung in XMLUnit 2.x.

1. Übersicht

XMLUnit 2.x ist eine leistungsstarke Bibliothek, mit der wir XML-Inhalte testen und überprüfen können. Sie ist besonders nützlich, wenn wir genau wissen, was XML enthalten sollte.

Daher verwenden wir XMLUnit hauptsächlich in Komponententests , um zu überprüfen, ob es sich bei dem vorhandenen XML um gültiges XML handelt , ob es bestimmte Informationen enthält oder einem bestimmten Stildokument entspricht.

Darüber hinaus haben wir mit XMLUnit die Kontrolle darüber, welche Art von Unterschied für uns wichtig ist und welcher Teil der Stilreferenz mit welchem ​​Teil Ihres Vergleichs-XML verglichen werden soll.

Da wir uns auf XMLUnit 2.x und nicht auf XMLUnit 1.x konzentrieren, beziehen wir uns bei jeder Verwendung des Wortes XMLUnit streng auf 2.x.

Schließlich werden wir auch Hamcrest-Matcher für Behauptungen verwenden. Daher ist es eine gute Idee, Hamcrest aufzufrischen, falls Sie nicht damit vertraut sind.

2. XMLUnit Maven Setup

Um die Bibliothek in unseren Maven-Projekten verwenden zu können, müssen die folgenden Abhängigkeiten in pom.xml vorhanden sein :

 org.xmlunit xmlunit-core 2.2.1 

Die neueste Version von xmlunit-core finden Sie unter folgendem Link. Und:

 org.xmlunit xmlunit-matchers 2.2.1 

Die neueste Version von xmlunit-matchers ist unter diesem Link verfügbar.

3. XML vergleichen

3.1. Einfache Differenzbeispiele

Nehmen wir an, wir haben zwei XML-Teile. Sie gelten als identisch, wenn Inhalt und Reihenfolge der Knoten in den Dokumenten genau gleich sind. Daher besteht der folgende Test:

@Test public void given2XMLS_whenIdentical_thenCorrect() { String controlXml = "3false"; String testXml = "3false"; assertThat(testXml, CompareMatcher.isIdenticalTo(controlXml)); }

Dieser nächste Test schlägt fehl, da die beiden XML-Teile ähnlich, aber nicht identisch sind, da ihre Knoten in einer anderen Reihenfolge auftreten :

@Test public void given2XMLSWithSimilarNodesButDifferentSequence_whenNotIdentical_thenCorrect() { String controlXml = "3false"; String testXml = "false3"; assertThat(testXml, assertThat(testXml, not(isIdenticalTo(controlXml))); }

3.2. Detailliertes Differenzbeispiel

Unterschiede zwischen zwei oben genannten XML-Dokumenten werden von der Difference Engine erkannt .

Standardmäßig und aus Effizienzgründen wird der Vergleichsprozess gestoppt, sobald der erste Unterschied festgestellt wird.

Um alle Unterschiede zwischen zwei XML-Elementen zu ermitteln, verwenden wir eine Instanz der Diff- Klasse wie folgt:

@Test public void given2XMLS_whenGeneratesDifferences_thenCorrect(){ String controlXml = "3false"; String testXml = "false3"; Diff myDiff = DiffBuilder.compare(controlXml).withTest(testXml).build(); Iterator iter = myDiff.getDifferences().iterator(); int size = 0; while (iter.hasNext()) { iter.next().toString(); size++; } assertThat(size, greaterThan(1)); }

Wenn wir die in der while- Schleife zurückgegebenen Werte drucken , ist das Ergebnis wie folgt:

Expected element tag name 'int' but was 'boolean' - comparing  at /struct[1]/int[1] to  at /struct[1]/boolean[1] (DIFFERENT) Expected text value '3' but was 'false' - comparing 3 at /struct[1]/int[1]/text()[1] to false at /struct[1]/boolean[1]/text()[1] (DIFFERENT) Expected element tag name 'boolean' but was 'int' - comparing  at /struct[1]/boolean[1] to  at /struct[1]/int[1] (DIFFERENT) Expected text value 'false' but was '3' - comparing false at /struct[1]/boolean[1]/text()[1] to 3 at /struct[1]/int[1]/text()[1] (DIFFERENT)

Jede Instanz beschreibt sowohl die Art des Unterschieds zwischen einem Steuerknoten und einem Testknoten als auch die Details dieser Knoten (einschließlich des XPath-Standorts jedes Knotens).

Wenn wir die Difference Engine zwingen möchten, anzuhalten, nachdem der erste Unterschied gefunden wurde, und keine weiteren Unterschiede aufzählen möchten, müssen wir einen ComparisonController bereitstellen :

@Test public void given2XMLS_whenGeneratesOneDifference_thenCorrect(){ String myControlXML = "3false"; String myTestXML = "false3"; Diff myDiff = DiffBuilder .compare(myControlXML) .withTest(myTestXML) .withComparisonController(ComparisonControllers.StopWhenDifferent) .build(); Iterator iter = myDiff.getDifferences().iterator(); int size = 0; while (iter.hasNext()) { iter.next().toString(); size++; } assertThat(size, equalTo(1)); }

Die Differenzmeldung ist einfacher:

Expected element tag name 'int' but was 'boolean' - comparing  at /struct[1]/int[1] to  at /struct[1]/boolean[1] (DIFFERENT)

4. Eingabequellen

Mit XMLUnit können wir XML-Daten aus einer Vielzahl von Quellen auswählen , die für die Anforderungen unserer Anwendung geeignet sind. In diesem Fall verwenden wir die Input- Klasse mit ihrem Array statischer Methoden.

Um Eingaben aus einer XML-Datei im Projektstamm auszuwählen, gehen Sie wie folgt vor:

@Test public void givenFileSource_whenAbleToInput_thenCorrect() { ClassLoader classLoader = getClass().getClassLoader(); String testPath = classLoader.getResource("test.xml").getPath(); String controlPath = classLoader.getResource("control.xml").getPath(); assertThat( Input.fromFile(testPath), isSimilarTo(Input.fromFile(controlPath))); }

So wählen Sie eine Eingabequelle aus einer XML-Zeichenfolge aus:

@Test public void givenStringSource_whenAbleToInput_thenCorrect() { String controlXml = "3false"; String testXml = "3false"; assertThat( Input.fromString(testXml),isSimilarTo(Input.fromString(controlXml))); }

Verwenden wir jetzt einen Stream als Eingabe:

@Test public void givenStreamAsSource_whenAbleToInput_thenCorrect() { assertThat(Input.fromStream(XMLUnitTests.class .getResourceAsStream("/test.xml")), isSimilarTo( Input.fromStream(XMLUnitTests.class .getResourceAsStream("/control.xml")))); }

Wir könnten auch Input.from (Object) verwenden, wo wir eine gültige Quelle übergeben, die von XMLUnit aufgelöst werden soll.

Zum Beispiel können wir eine Datei übergeben in:

@Test public void givenFileSourceAsObject_whenAbleToInput_thenCorrect() { ClassLoader classLoader = getClass().getClassLoader(); assertThat( Input.from(new File(classLoader.getResource("test.xml").getFile())), isSimilarTo(Input.from(new File(classLoader.getResource("control.xml").getFile())))); }

Oder eine Zeichenfolge:

@Test public void givenStringSourceAsObject_whenAbleToInput_thenCorrect() { assertThat( Input.from("3false"), isSimilarTo(Input.from("3false"))); }

Oder ein Stream:

@Test public void givenStreamAsObject_whenAbleToInput_thenCorrect() { assertThat( Input.from(XMLUnitTest.class.getResourceAsStream("/test.xml")), isSimilarTo(Input.from(XMLUnitTest.class.getResourceAsStream("/control.xml")))); }

und sie werden alle gelöst sein.

5. Vergleichen bestimmter Knoten

In Abschnitt 2 oben haben wir uns nur mit identischem XML befasst, da für ähnliches XML einige Anpassungen mithilfe der Funktionen der xmlunit-core- Bibliothek erforderlich sind :

@Test public void given2XMLS_whenSimilar_thenCorrect() { String controlXml = "3false"; String testXml = "false3"; assertThat(testXml, isSimilarTo(controlXml)); }

Der obige Test sollte bestanden werden, da die XMLs ähnliche Knoten haben, er schlägt jedoch fehl. Dies liegt daran, dass XMLUnit Steuerungs- und Testknoten in derselben Tiefe relativ zum Stammknoten vergleicht .

So an isSimilarTo condition is a little bit more interesting to test than an isIdenticalTo condition. The node 3 in controlXml will be compared with false in testXml, automatically giving failure message:

java.lang.AssertionError: Expected: Expected element tag name 'int' but was 'boolean' - comparing  at /struct[1]/int[1] to  at /struct[1]/boolean[1]: 3 but: result was: false

This is where the DefaultNodeMatcher and ElementSelector classes of XMLUnit come in handy

The DefaultNodeMatcher class is consulted by XMLUnit at comparison stage as it loops over nodes of controlXml, to determine which XML node from testXml to compare with the current XML node it encounters in controlXml.

Before that, DefaultNodeMatcher will have already consulted ElementSelector to decide how to match nodes.

Our test has failed because in the default state, XMLUnit will use a depth-first approach to traversing the XMLs and based on document order to match nodes, hence is matched with .

Let's tweak our test so that it passes:

@Test public void given2XMLS_whenSimilar_thenCorrect() { String controlXml = "3false"; String testXml = "false3"; assertThat(testXml, isSimilarTo(controlXml).withNodeMatcher( new DefaultNodeMatcher(ElementSelectors.byName))); }

In this case, we are telling DefaultNodeMatcher that when XMLUnit asks for a node to compare, you should have sorted and matched the nodes by their element names already.

The initial failed example was similar to passing ElementSelectors.Default to DefaultNodeMatcher.

Alternatively, we could have used a Diff from xmlunit-core rather than using xmlunit-matchers:

@Test public void given2XMLs_whenSimilarWithDiff_thenCorrect() throws Exception { String myControlXML = "3false"; String myTestXML = "false3"; Diff myDiffSimilar = DiffBuilder.compare(myControlXML).withTest(myTestXML) .withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byName)) .checkForSimilar().build(); assertFalse("XML similar " + myDiffSimilar.toString(), myDiffSimilar.hasDifferences()); }

6. Custom DifferenceEvaluator

A DifferenceEvaluator makes determinations of the outcome of a comparison. Its role is restricted to determining the severity of a comparison's outcome.

It's the class that decides whether two XML pieces are identical, similar or different.

Consider the following XML pieces:

and:

In the default state, they are technically evaluated as different because their attr attributes have different values. Let's take a look at a test:

@Test public void given2XMLsWithDifferences_whenTestsDifferentWithoutDifferenceEvaluator_thenCorrect(){ final String control = ""; final String test = ""; Diff myDiff = DiffBuilder.compare(control).withTest(test) .checkForSimilar().build(); assertFalse(myDiff.toString(), myDiff.hasDifferences()); }

Failure message:

java.lang.AssertionError: Expected attribute value 'abc' but was 'xyz' - comparing  at /a[1]/b[1]/@attr to  at /a[1]/b[1]/@attr

If we don't really care about the attribute, we can change the behavior of DifferenceEvaluator to ignore it. We do this by creating our own:

public class IgnoreAttributeDifferenceEvaluator implements DifferenceEvaluator { private String attributeName; public IgnoreAttributeDifferenceEvaluator(String attributeName) { this.attributeName = attributeName; } @Override public ComparisonResult evaluate(Comparison comparison, ComparisonResult outcome) { if (outcome == ComparisonResult.EQUAL) return outcome; final Node controlNode = comparison.getControlDetails().getTarget(); if (controlNode instanceof Attr) { Attr attr = (Attr) controlNode; if (attr.getName().equals(attributeName)) { return ComparisonResult.SIMILAR; } } return outcome; } }

We then rewrite our initial failed test and supply our own DifferenceEvaluator instance, like so:

@Test public void given2XMLsWithDifferences_whenTestsSimilarWithDifferenceEvaluator_thenCorrect() { final String control = ""; final String test = ""; Diff myDiff = DiffBuilder.compare(control).withTest(test) .withDifferenceEvaluator(new IgnoreAttributeDifferenceEvaluator("attr")) .checkForSimilar().build(); assertFalse(myDiff.toString(), myDiff.hasDifferences()); }

This time it passes.

7. Validation

XMLUnit performs XML validation using the Validator class. You create an instance of it using the forLanguage factory method while passing in the schema to use in validation.

The schema is passed in as a URI leading to its location, XMLUnit abstracts the schema locations it supports in the Languages class as constants.

We typically create an instance of Validator class like so:

Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI);

After this step, if we have our own XSD file to validate against our XML, we simply specify its source and then call Validator‘s validateInstance method with our XML file source.

Take for example our students.xsd:

And students.xml:

 Rajiv 18 Candie 19 

Let's then run a test:

@Test public void givenXml_whenValidatesAgainstXsd_thenCorrect() { Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI); v.setSchemaSource(Input.fromStream( XMLUnitTests.class.getResourceAsStream("/students.xsd")).build()); ValidationResult r = v.validateInstance(Input.fromStream( XMLUnitTests.class.getResourceAsStream("/students.xml")).build()); Iterator probs = r.getProblems().iterator(); while (probs.hasNext()) { probs.next().toString(); } assertTrue(r.isValid()); }

The result of the validation is an instance of ValidationResult which contains a boolean flag indicating whether the document has been validated successfully.

The ValidationResult also contains an Iterable with ValidationProblems in case there is a failure. Let's create a new XML with errors called students_with_error.xml. Instead of , our starting tags are all :

 Rajiv 18 Candie 19 

Then run this test against it:

@Test public void givenXmlWithErrors_whenReturnsValidationProblems_thenCorrect() { Validator v = Validator.forLanguage(Languages.W3C_XML_SCHEMA_NS_URI); v.setSchemaSource(Input.fromStream( XMLUnitTests.class.getResourceAsStream("/students.xsd")).build()); ValidationResult r = v.validateInstance(Input.fromStream( XMLUnitTests.class.getResourceAsStream("/students_with_error.xml")).build()); Iterator probs = r.getProblems().iterator(); int count = 0; while (probs.hasNext()) { count++; probs.next().toString(); } assertTrue(count > 0); }

If we were to print the errors in the while loop, they would look like:

ValidationProblem { line=3, column=19, type=ERROR,message='cvc-complex-type.2.4.a: Invalid content was found starting with element 'studet'. One of '{student}' is expected.' } ValidationProblem { line=6, column=4, type=ERROR, message="The element type "studet" must be terminated by the matching end-tag ""." } ValidationProblem { line=6, column=4, type=ERROR, message="The element type "studet" must be terminated by the matching end-tag ""." }

8. XPath

When an XPath expression is evaluated against a piece of XML a NodeList is created that contains the matching Nodes.

Consider this piece of XML saved in a file called teachers.xml:

 math physics political education english 

XMLUnit offers a number of XPath related assertion methods, as demonstrated below.

We can retrieve all the nodes called teacher and perform assertions on them individually:

@Test public void givenXPath_whenAbleToRetrieveNodes_thenCorrect() { Iterable i = new JAXPXPathEngine() .selectNodes("//teacher", Input.fromFile(new File("teachers.xml")).build()); assertNotNull(i); int count = 0; for (Iterator it = i.iterator(); it.hasNext();) { count++; Node node = it.next(); assertEquals("teacher", node.getNodeName()); NamedNodeMap map = node.getAttributes(); assertEquals("department", map.item(0).getNodeName()); assertEquals("id", map.item(1).getNodeName()); assertEquals("teacher", node.getNodeName()); } assertEquals(2, count); }

Notice how we validate the number of child nodes, the name of each node and the attributes in each node. Many more options are available after retrieving the Node.

To verify that a path exists, we can do the following:

@Test public void givenXmlSource_whenAbleToValidateExistingXPath_thenCorrect() { assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//teachers")); assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//teacher")); assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//subject")); assertThat(Input.fromFile(new File("teachers.xml")), hasXPath("//@department")); }

To verify that a path does not exist, this is what we can do:

@Test public void givenXmlSource_whenFailsToValidateInExistentXPath_thenCorrect() { assertThat(Input.fromFile(new File("teachers.xml")), not(hasXPath("//sujet"))); }

XPaths are especially useful where a document is made up largely of known, unchanging content with only a small amount of changing content created by the system.

9. Conclusion

In this tutorial, we have introduced most of the basic features of XMLUnit 2.x and how to use them to validate XML documents in our applications.

The full implementation of all these examples and code snippets can be found in the XMLUnit GitHub project.