Introduction à XMLUnit 2.x

1. Vue d'ensemble

XMLUnit 2.x est une bibliothèque puissante qui nous aide à tester et à vérifier le contenu XML, et est particulièrement utile lorsque nous savons exactement ce que ce XML doit contenir.

Nous utiliserons donc principalement XMLUnit dans des tests unitaires pour vérifier que ce que nous avons est du XML valide , qu'il contient certaines informations ou est conforme à un certain style de document.

De plus, avec XMLUnit, nous contrôlons quel type de différence est important pour nous et quelle partie de la référence de style comparer avec quelle partie de votre XML de comparaison.

Puisque nous nous concentrons sur XMLUnit 2.x et non sur XMLUnit 1.x, chaque fois que nous utilisons le mot XMLUnit, nous nous référons strictement à 2.x.

Enfin, nous utiliserons également des correspondants Hamcrest pour les assertions, c'est donc une bonne idée de revoir Hamcrest au cas où vous ne le seriez pas familier.

2. Configuration de XMLUnit Maven

Pour utiliser la bibliothèque dans nos projets maven, nous devons avoir les dépendances suivantes dans pom.xml :

 org.xmlunit xmlunit-core 2.2.1 

La dernière version de xmlunit-core peut être trouvée en suivant ce lien. Et:

 org.xmlunit xmlunit-matchers 2.2.1 

La dernière version de xmlunit-matchers est disponible sur ce lien.

3. Comparaison de XML

3.1. Exemples de différences simples

Supposons que nous ayons deux morceaux de XML. Ils sont réputés identiques lorsque le contenu et la séquence des nœuds dans les documents sont exactement les mêmes, de sorte que le test suivant réussira:

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

Ce test suivant échoue car les deux morceaux de XML sont similaires mais pas identiques car leurs nœuds se produisent dans une séquence différente :

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

3.2. Exemple de différence détaillé

Les différences entre les deux documents XML ci-dessus sont détectées par le moteur de différence .

Par défaut et pour des raisons d'efficacité, il arrête le processus de comparaison dès que la première différence est trouvée.

Pour obtenir toutes les différences entre deux morceaux de XML, nous utilisons une instance de la classe Diff comme ceci:

@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)); }

Si nous imprimons les valeurs retournées dans le tout en boucle, le résultat est comme ci - dessous:

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)

Chaque instance décrit à la fois le type de différence trouvée entre un nœud de contrôle et un nœud de test et le détail de ces nœuds (y compris l'emplacement XPath de chaque nœud).

Si nous voulons forcer l' arrêt du moteur de différence après la découverte de la première différence et ne pas procéder à l'énumération d'autres différences, nous devons fournir un ComparisonController :

@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)); }

Le message de différence est plus simple:

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

4. Sources d'entrée

Avec XMLUnit , nous pouvons sélectionner des données XML à partir d'une variété de sources qui peuvent convenir aux besoins de notre application. Dans ce cas, nous utilisons la classe Input avec son tableau de méthodes statiques.

Pour sélectionner l'entrée d'un fichier XML situé à la racine du projet, nous procédons comme suit:

@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))); }

Pour choisir une source d'entrée à partir d'une chaîne XML, comme ceci:

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

Utilisons maintenant un flux comme entrée:

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

Nous pourrions également utiliser Input.from (Object) où nous transmettons n'importe quelle source valide à résoudre par XMLUnit.

For example, we can pass a file 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())))); }

Or a String:

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

Or a Stream:

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

and they will all be resolved.

5. Comparing Specific Nodes

In section 2 above, we only looked at identical XML because similar XML needs a little bit of customization using features from xmlunit-core library:

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

The above test should pass since the XMLs have similar nodes, however, it fails. This is because XMLUnit compares control and test nodes at the same depth relative to the root node.

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.