Introduction à XMLUnit 2.x

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 pratique lorsque nous savons exactement ce que ce XML doit contenir.

Nous utiliserons donc principalement XMLUnit dans les tests unitairesto verify that what we have is valid XML, qui contiennent certaines informations ou sont conformes à un certain style de document.

De plus, avec XMLUnit,we have control over what kind of difference is important to us 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 pas XMLUnit 1.x, chaque fois que nous utilisons le mot XMLUnit, nous faisons strictement référence à 2.x.

Enfin, nous utiliserons également des matchers Hamcrest pour les assertions, c'est donc une bonne idée de revoirHamcrest 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 danspom.xml:


    org.xmlunit
    xmlunit-core
    2.2.1

La dernière version dexmlunit-core peut être trouvée en suivantthis link. And:


    org.xmlunit
    xmlunit-matchers
    2.2.1

La dernière version dexmlunit-matchers est disponible àthis link.

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 identiques, le test suivant réussit:

@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 à leursnodes occur in a different sequence:

@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 lesDifference Engine.

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 classeDiff 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 renvoyées dans la bouclewhile, le résultat est le suivant:

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é entre un nœud de contrôle et un nœud de test, ainsi que le détail de ces nœuds (y compris l'emplacement XPath de chaque nœud).

Si nous voulons forcer le moteur de différence àstop after the first difference is found et ne pas continuer à énumérer d'autres différences, nous devons fournir unComparisonController:

@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 de diverses sources qui peuvent convenir aux besoins de notre application. Dans ce cas, nous utilisons la classeInput avec son tableau de méthodes statiques.

Pour sélectionner une entrée dans un fichier XML situé à la racine du projet, procédez 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, procédez comme suit:

@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 utiliserInput.from(Object) où nous transmettons n'importe quelle source valide à résoudre par XMLUnit.

Par exemple, nous pouvons passer un fichier dans:

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

Ou unString:

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

Ou unStream:

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

et ils seront tous résolus.

5. Comparaison de nœuds spécifiques

Dans la section 2 ci-dessus, nous n'avons examiné que du XML identique car un XML similaire nécessite un peu de personnalisation à l'aide des fonctionnalités de la bibliothèquexmlunit-core:

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

    assertThat(testXml, isSimilarTo(controlXml));
}

Le test ci-dessus devrait réussir puisque les XML ont des nœuds similaires, cependant, il échoue. C'est parce queXMLUnit compares control and test nodes at the same depth relative to the root node.

Une conditionisSimilarTo est donc un peu plus intéressante à tester qu'une conditionisIdenticalTo. Le nœud<int>3</int> encontrolXml sera comparé à<boolean>false</boolean> entestXml, donnant automatiquement un message d'échec:

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

C'est là que les classesDefaultNodeMatcher etElementSelector de XMLUnit sont utiles

La classeDefaultNodeMatcher est consultée par XMLUnit au stade de la comparaison car elle boucle sur les nœuds decontrolXml, pour déterminer quel nœud XML detestXml à comparer avec le nœud XML actuel qu'il rencontre danscontrolXml.

Avant cela,DefaultNodeMatcher aura déjà consultéElementSelector pour décider comment faire correspondre les nœuds.

Notre test a échoué car dans l'état par défaut, XMLUnit utilisera une approche en profondeur pour parcourir les XML et basée sur l'ordre des documents pour faire correspondre les nœuds, par conséquent<int> correspond à<boolean>.

Modifions notre test pour qu'il réussisse:

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

    assertThat(testXml,
      isSimilarTo(controlXml).withNodeMatcher(
      new DefaultNodeMatcher(ElementSelectors.byName)));
}

Dans ce cas, nous disons àDefaultNodeMatcher que lorsque XMLUnit demande un nœud à comparer, vous devriez déjà avoir trié et mis en correspondance les nœuds par leurs noms d'élément.

L'exemple d'échec initial était similaire au passage deElementSelectors.Default àDefaultNodeMatcher.

Alternativement, nous aurions pu utiliser unDiff dexmlunit-core plutôt que d'utiliserxmlunit-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. PersonnaliséDifferenceEvaluator

UnDifferenceEvaluator détermine le résultat d'une comparaison. Son rôle se limite à déterminer la gravité du résultat d’une comparaison.

C’est la classe qui décide si deux éléments XML sontidentical,similar oudifferent.

Considérez les éléments XML suivants:

and:

Dans l'état par défaut, ils sont techniquement évalués comme différents car leurs attributsattr ont des valeurs différentes. Jetons un coup d'œil à un 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());
}

Message d'échec:

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

Si nous ne nous soucions pas vraiment de l’attribut, nous pouvons changer le comportement deDifferenceEvaluator pour l’ignorer. Nous faisons cela en créant notre propre:

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

Nous réécrivons ensuite notre test initial qui a échoué et fournissons notre propre instanceDifferenceEvaluator, comme ceci:

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

Cette fois ça passe.

7. Validation

XMLUnit effectue la validation XML à l'aide de la classeValidator. Vous en créez une instance à l'aide de la méthode de fabriqueforLanguage tout en passant le schéma à utiliser dans la validation.

Le schéma est transmis en tant qu'URI menant à son emplacement, XMLUnit fait abstraction des emplacements de schéma qu'il prend en charge dans la classeLanguages en tant que constantes.

Nous créons généralement une instance de la classeValidator comme ceci:

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

Après cette étape, si nous avons notre propre fichier XSD à valider par rapport à notre XML, nous spécifions simplement sa source puis appelons la méthodevalidateInstance deValidator avec notre source de fichier XML.

Prenons par exemple nosstudents.xsd:



    
        
            
                
            
        
    
    
        
            
            
        
        
    

Etstudents.xml:



    
        Rajiv
        18
    
    
        Candie
        19
    

Faisons ensuite un 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());
}

Le résultat de la validation est une instance deValidationResult qui contient un indicateur booléen indiquant si le document a été validé avec succès.

LeValidationResult contient également unIterable avecValidationProblems en cas de panne. Créons un nouveau XML avec des erreurs appeléesstudents_with_error.xml. Au lieu de<student>, nos balises de départ sont toutes</studet>:



    
        Rajiv
        18
    
    
        Candie
        19
    

Puis lancez ce test contre:

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

Si nous devions afficher les erreurs dans la bouclewhile, elles ressembleraient à:

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

Lorsqu'une expression XPath est évaluée par rapport à un élément XML, unNodeList est créé qui contient lesNodes. correspondants

Considérez ce morceau de XML enregistré dans un fichier appeléteachers.xml:


    
        math
        physics
    
    
        political education
        english
    

XMLUnit propose un certain nombre de méthodes d'assertion liées à XPath, comme illustré ci-dessous.

Nous pouvons récupérer tous les nœuds appelésteacher et effectuer des assertions sur eux individuellement:

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

Remarquez comment nous validons le nombre de nœuds enfants, le nom de chaque nœud et les attributs de chaque nœud. De nombreuses autres options sont disponibles après la récupération desNode.

Pour vérifier qu’un chemin existe, nous pouvons procéder comme suit:

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

Pour vérifier qu’un chemin n’existe pas, voici ce que nous pouvons faire:

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

Les XPaths sont particulièrement utiles lorsqu'un document est constitué en grande partie de contenu connu, immuable, avec seulement une petite quantité de contenu modifié créé par le système.

9. Conclusion

Dans ce tutoriel, nous avons présenté la plupart des fonctionnalités de base deXMLUnit 2.x et comment les utiliser pour valider des documents XML dans nos applications.

L'implémentation complète de tous ces exemples et extraits de code peut être trouvée dans lesXMLUnitGitHub project.