Java

Hamcrest est une bibliothèque d'objets de correspondance ('matchers' ou contraintes ou encore prédicats) permettant de définir des règles de 'correspondance' de façon déclarative, utilisables dans d'autres frameworks. Typiquement, on l'utilisera avec des frameworks de test, des bibliothèques d'objets bouchons (mock objects) et des règles de validation d'interface utilisateur.Par exemple, au lieu d'écrire:

   assertEquals("bleu", couleur);

On écrira:

   assertThat(couleur,is("bleu"));

On note ici le gain immédiat de lisibilité (surtout en anglais)...

Hamcrest a été implémenté pour Java, PHP mais aussi C++, Objective-C, Python et Erlang. Naturellement, avec Java, on pourra gérer cette dépendance via Maven.

Une version d'Hamcrest est fournie avec JUnit. Cependant, elle date un peu et les versions plus récentes d'Hamcrest offrent un tas de nouvelles fonctionnalités, en particuliers pour travailler avec les collections. Vous pouvez utiliser la dernière version d'Hamcrest en utilisant la dépendance junit-dep à la place de junit, comme suit:

  <dependency>
      <groupId>junit</groupId>
      <artifactId>junit-dep</artifactId>
      <version>4.10</version>
      <scope>test</scope>
      <exclusions>
         <exclusion>
              <groupId>org.hamcrest</groupId>
              <artifactId>hamcrest-core</artifactId>
          </exclusion>
      </exclusions>
  </dependency>
  <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-library</artifactId>
      <version>1.3.RC2</version>
  </dependency>

junit-dep est exactement la même bibliothèque que junit, exception faite que ses dépendances sont explicitement déclarées et non incluse dans le bundle.....

Notez enfin que Hamcrest n'est pas une bibliothèque de test comme JUnit ou TestNG, mais bien une bibliothèque d'objets de correspondance destinés à rendre les tests implémentés avec les bibliothèques précédentes beaucoup plus lisibles.

Ce qui suit est la traduction du tutoriel Java que l'on peut lire sur le wiki du site de Hamcrest. On pourra également lire ce billet de John Smart.

Introduction

Hamcrest est un framework pour écrire des objets de correspondance (matchers) permettant d'écrire déclarativement les règles de correspondance. Il y a nombre de situations pour lesquels les matchers sont inestimables, comme lors de la validation d'IHM, le filtrage de données mais c'est dans le domaine de l'écriture des tests flexibles que les matchers sont le plus communément utilisés. Ce tutoriel va montrer comment utiliser Hamcrest pour les tests unitaires.

Quand on écrit des tests, il est parfois difficile de trouver le juste équilibre entre sur spécifier le test (et le rendre fragile face au changement) et ne pas le spécifier suffisamment (rendant le test moins de moindre valeur puisqu'il continuer à passer même quand la chose testée est cassée). Avoir un outil qui vous permet de cerner précisément l'aspect à tester et de décrire les valeurs qu'il doit avoir à un niveau de précision contrôlé aide grandement à écrire des tests qui sont "justes adaptés". De tels tests échouent quand le comportement de l'aspect qui est testé dévie du comportement attendu, mais continuent à réussir quand des modifications mineures et sans liens avec le comportement sont faites.

Mon premier test Hamcrest

Nous allons commencer à écrire un test JUnit 3 très simple, mais au lieu d'utiliser les méthodes JUnit assertEquals, nous utiliseront la construction Hamcrest assertThat et la série standard des matchers, les deux étant importés statiquement:

  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.hamcrest.Matchers.*;
  
  import junit.framework.TestCase;
  
  public class BiscuitTest extends TestCase {
    public void testEquals() {
      Biscuit theBiscuit = new Biscuit("Ginger");
      Biscuit myBiscuit = new Biscuit("Ginger");
      assertThat(theBiscuit, equalTo(myBiscuit));
    }
  }

La méthode assertThat est une phrase stylisée pour faire une affirmation de test. Dans cet exemple, le sujet de l'affirmation est l'objet Biscuit qui est le premier paramètre de la méthode. Le second paramètre de la méthode est un matcher pour les objets Biscuits, ici un matcher qui contrôle qu'un objet est égal à un autre en utilisant la méthode equals d'Object. Le test réussit puisque la classe Biscuit définit une méthode equals.

Si vous avez plus d'une affirmation dans votre test, vous pouvez inclure un identifiant (le premier paramètre) pour la valeur testée dans l'affirmation:

  assertThat("chocolate chips", theBiscuit.getChocolateChipCount(), equalTo(10));
  assertThat("hazelnuts", theBiscuit.getHazelnutCount(), equalTo(3));

Autres frameworks de test

Hamcrest a été conçu dès le début pour s'intégrer avec différents frameworks de tests unitaires. Par exemple, Hamcrest peut être utilisé avec JUnit 3 et 4 et TestNG. (Pour avoir des détails, voir les exemples qui sont inclus dans la distribution complète de Hamcrest). Il est assez facile de migrer une suite de tests existante en utilisant des affirmations en style Hamcrest puisque les autres styles d'affirmation peuvent co-exister avec celles d'Hamcrest.

Hamcrest peut également être utilisé avec des frameworks d'objets bouchons (mock object) en utilisant des adaptateurs pour faire le pont entre le concept de matcher des frameworks d'objets bouchons et un matcher Hamcrest. Par exemple, les contraintes JMock 1 sont des matchers d'Hamcrest. Hamcrest fournit un adaptateur JMock 1 pour vous permettre d'utiliser les matchers Hamcrest dans vos tests JMock 1. JMock 2 n'a pas besoin d'une telle couche d'adaptation puisqu'il est conçu pour utiliser Hamcrest comme bibliothèque de matchers. Hamcrest fournit également des adaptateurs pour EasyMock 2. De nouveau, voir les examples Hamcrest pour plus de détails.

Un tour des matchers courants

Hamcrest offre une bibliothèque de matchers utiles. En voici certains des plus importants.

  • Noyau
    • anything - correspond toujours, utile si vous ne vous souciez pas de l'objet qui est testé
    • describedAs - décorateur pour ajouter une description d'échec personnalisé
    • is - décorateur pour améliorer la lisibilité - voir "Sucre" ci-dessous
  • Logique
    • allOf - correspond si tous les matchers correspondent, fonctionne en court-circuit (arrêt à la première non correspondance, comme le && Java)
    • anyOf - correspond si l'un des matchers correspond, fonctionne en court-circuit (arrêt à la première correspondance, comme le || Java)
    • not - correspond si le matcher enrobé ne correspond pas et vice-versa
  • Objet
    • equalTo - test d'égalité des objets en utilisant Object.equals
    • hasToString - teste Object.toString
    • instanceOf, isCompatibleType - teste le type
    • notNullValue, nullValue - teste pour null
    • sameInstance - test l'identité d'objets
  • Beans
    • hasProperty - teste les propriétés JavaBeans
  • Collections
    • array - teste les éléments d'un tableau vis-à-vis d'un tableau de matchers
    • hasEntry, hasKey, hasValue - teste si un map contient une entrée, une clef ou une valeur
    • hasItem, hasItems - teste si une collection contient des éléments
    • hasItemInArray - teste si un tableau contient un élément
  • Nombre
    • closeTo - teste si une valeur réelle est proche d'une valeur donnée
    • greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo - teste l'ordre
  • Texte
    • equalToIgnoringCase - teste l'égalité de chaînes de caractères en ignorant la casse
    • equalToIgnoringWhiteSpace - teste l'égalité de chaînes de caractères en ignorant les différences d'espace
    • containsString, endsWith, startsWith - teste la correspondance de chaînes de caractères

Sucre

Hamcrest s'efforce de rendre vos tests aussi lisibles que possible. Par exemple, le matcher is est un enrobeur qui n'ajoute aucun comportement supplémentaire au matcher sous-jacent. Les affirmations suivantes sont toutes équivalentes:

  assertThat(theBiscuit, equalTo(myBiscuit));
  assertThat(theBiscuit, is(equalTo(myBiscuit)));
  assertThat(theBiscuit, is(myBiscuit));

La dernière forme est autorisée puisque is(T valeur) est surchargé pour renvoyer is(equalTo(valeur)).

Ecrire des matchers personnalisés

Hamcrest vient avec plein de matchers utiles, mais vous aurez probablement besoin de créer les vôtres de temps en temps pour correspondre à vos besoins de test. Ceci arrive communément quand vous trouvez un morceau de code qui teste la même série de propriétés encore et encore (et dans différents tests) et que vous voulez empaqueter ce morceau de code dans une unique affirmation. En écrivant votre propre matcher, vous allez éliminer la duplication de code et rendre vos tests plus lisibles !

Ecrivons notre propre matcher pour tester si une valeur réelle a la valeur NaN (not a number, n'est pas un nombre). Voici le test que nous voulons écrire:

  public void testSquareRootOfMinusOneIsNotANumber() {
    assertThat(Math.sqrt(-1), is(notANumber()));
  }

Et voici l'implémentation:

  package org.hamcrest.examples.tutorial;
  
  import org.hamcrest.Description;
  import org.hamcrest.Factory;
  import org.hamcrest.Matcher;
  import org.hamcrest.TypeSafeMatcher;
  
  public class IsNotANumber extends TypeSafeMatcher<Double> {
  
    @Override
    public boolean matchesSafely(Double number) {
      return number.isNaN();
    }
  
    public void describeTo(Description description) {
      description.appendText("not a number");
    }
  
    @Factory
    public static <T> Matcher<Double> notANumber() {
      return new IsNotANumber();
    }
  
  }

La méthode assertThat est une méthode générique qui prend un Matcher paramétré par le type du sujet de l'affirmation. Nous affirmons des choses relatives aux valeurs doubles, aussi nous savons que nous avons besoin d'un Matcher<Double>. Pour l'implémentation de notre matcher, il est plus pratique d'hériter de TypeSafeMatcher qui réalise le cast d'un Double pour nous. Nous n'avons besoin que d'implémenter la méthode matchSafely - qui vérifie simplement que le Double est un NaN - et la méthode describeTo - qui est utilisée pour produire un message d'échec quand le test échoue. Voici un exemple de ce à quoi le message d'échec pourrait ressembler:

assertThat(1.0, is(notANumber()));

  fails with the message
  
  java.lang.AssertionError: 
  Expected: is not a number
      got : <1.0>}}

La troisième méthode de notre matcher est une méthode pratique de fabrique. Nous importons statiquement cette méthode pour utiliser le matcher dans notre test:

  import static org.hamcrest.MatcherAssert.assertThat;
  import static org.hamcrest.Matchers.*;
  
  import static org.hamcrest.examples.tutorial.IsNotANumber.notANumber;
  
  import junit.framework.TestCase;
  
  public class NumberTest extends TestCase {
  
    public void testSquareRootOfMinusOneIsNotANumber() {
      assertThat(Math.sqrt(-1), is(notANumber()));
    }
  }

Bien que la méthode notANumber crée un nouveau matcher à chaque fois qu'elle est appelée, vous ne devez pas supposer que c'est le seul canevas d'utilisation de notre matcher. Vous devriez plutôt vous assurer que votre matcher est sans état de telle sorte qu'une unique instance puisse être réutilisée pour les correspondances.

Génération du sucre

Si vous produisez plus de quelques matchers personnalisés, il devient ennuyeux d'avoir à les importer tous un par un. Il serait bien d'être capable de les regrouper dans une unique classe, de telle sorte qu'ils puissent être importés en n'utilisant qu'un unique import statique comme pour les matchers de la bibliothèque Hamcrest. Hamcrest se rend utile ici en fournissant un moyen de faire ceci en utilisant un générateur.

D'abord, il faut créer un fichier XML de configuration listant toutes les classes Matcher qui doivent être cherchées par les méthodes fabriques annotées par l'annotation org.hamcrest.Factory. Par exemple:

  <matchers>
  
    <!- - Hamcrest library - ->
    <factory class="org.hamcrest.core.Is"/>
  
    <!- - Custom extension - ->
    <factory class="org.hamcrest.examples.tutorial.IsNotANumber"/>
  
  </matchers>

Ensuite, exécutez l'outil en ligne de commande org.hamcrest.generator.config.XmlConfigurator fourni avec Hamcrest. Cet outil prend le fichier de configuration XML et génère une unique classe Java qui inclut toutes les méthodes de fabrication indiquées par le fichier XML. L'exécuter sans argument affichera un message d'utilisation. Voici la sortie affichée pour l'exemple:

  // Generated source.
  package org.hamcrest.examples.tutorial;
  
  public class Matchers {
  
    public static <T> org.hamcrest.Matcher<T> is(T param1) {
      return org.hamcrest.core.Is.is(param1);
    }
  
    public static <T> org.hamcrest.Matcher<T> is(java.lang.Class<T> param1) {
      return org.hamcrest.core.Is.is(param1);
    }
  
    public static <T> org.hamcrest.Matcher<T> is(org.hamcrest.Matcher<T> param1) {
      return org.hamcrest.core.Is.is(param1);
    }
  
    public static <T> org.hamcrest.Matcher<java.lang.Double> notANumber() {
      return org.hamcrest.examples.tutorial.IsNotANumber.notANumber();
    }
  
  }

Enfin, nous pouvons mettre à jour notre test pour utiliser la nouvelle classe Matcher.

  import static org.hamcrest.MatcherAssert.assertThat;
  
  import static org.hamcrest.examples.tutorial.Matchers.*;
  
  import junit.framework.TestCase;
  
  public class CustomSugarNumberTest extends TestCase {
  
    public void testSquareRootOfMinusOneIsNotANumber() {
      assertThat(Math.sqrt(-1), is(notANumber()));
    }
  }

Notez que nous utilisons maintenant le matcher is de la bibliothèque Hamcrest importé de notre propre classe de Matchers.