Poum - Mot-clé - JMock 1De la qualité logicielle, méthodes agiles et outils logiciels libres ...2023-08-25T16:53:12+01:00Poumurn:md5:6b5c625a812aff31ffa1cd1f3648e14bDotclearHamcrest, tutoriel pour Javaurn:md5:510f9660f7111917ae6eacd5afcb6fa12012-02-22T23:39:00+01:002012-02-24T10:50:56+01:00PoumQualité logicielleCEasyMockErlangHamcrestJavaJMock 1JMock 2JUnitjunit-depmatchersMavenmock objectObjective-CPHPPythonTestNG<p><img src="http://philippe.poumaroux.free.fr/public/Java-logo.png" alt="Java" title="Java, fév. 2012" /></p>
<p><a href="http://philippe.poumaroux.free.fr/index.php?tag/Hamcrest">Hamcrest</a> est une bibliothèque d'objets de correspondance ('<a href="http://philippe.poumaroux.free.fr/index.php?tag/matchers">matchers</a>' 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 (<a href="http://philippe.poumaroux.free.fr/index.php?tag/mock%20object">mock object</a>s) et des règles de validation d'interface utilisateur.Par exemple, au lieu d'écrire:</p>
<pre> assertEquals("bleu", couleur);</pre>
<p>On écrira:</p>
<pre> assertThat(couleur,is("bleu"));</pre>
<p>On note ici le gain immédiat de lisibilité (surtout en anglais)...</p>
<p>Hamcrest a été implémenté pour <a href="http://philippe.poumaroux.free.fr/index.php?tag/Java">Java</a>, <a href="http://philippe.poumaroux.free.fr/index.php?tag/PHP">PHP</a> mais aussi <a href="http://philippe.poumaroux.free.fr/index.php?tag/C">C++</a>, <a href="http://philippe.poumaroux.free.fr/index.php?tag/Objective-C">Objective-C</a>, <a href="http://philippe.poumaroux.free.fr/index.php?tag/Python">Python</a> et <a href="http://philippe.poumaroux.free.fr/index.php?tag/Erlang">Erlang</a>. Naturellement, avec Java, on pourra gérer cette dépendance via <a href="http://philippe.poumaroux.free.fr/index.php?tag/Maven">Maven</a>.</p>
<p>Une version d'Hamcrest est fournie avec <a href="http://philippe.poumaroux.free.fr/index.php?tag/JUnit">JUnit</a>. 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 <a href="http://philippe.poumaroux.free.fr/index.php?tag/junit-dep">junit-dep</a> à la place de junit, comme suit:</p>
<pre> <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></pre>
<p>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.....</p>
<p>Notez enfin que Hamcrest n'est pas une bibliothèque de test comme <a href="http://philippe.poumaroux.free.fr/index.php?tag/JUnit">JUnit</a> ou <a href="http://philippe.poumaroux.free.fr/index.php?tag/TestNG">TestNG</a>, 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.</p>
<p>Ce qui suit est la traduction du tutoriel Java que l'on peut <a href="http://code.google.com/p/hamcrest/" hreflang="en">lire sur le wiki du site de Hamcrest</a>. On pourra également <a href="http://weblogs.java.net/blog/johnsmart/archive/2011/12/12/some-useful-new-hamcrest-matchers-collections" hreflang="en">lire ce billet de John Smart</a>.</p> <h2>Introduction</h2>
<p>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.</p>
<p>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.</p>
<h2>Mon premier test Hamcrest</h2>
<p>Nous allons commencer à écrire un test JUnit 3 très simple, mais au lieu d'utiliser les méthodes JUnit <code>assertEquals</code>, nous utiliseront la construction Hamcrest <code>assertThat</code> et la série standard des matchers, les deux étant importés statiquement:</p>
<pre> 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));
}
}</pre>
<p>La méthode <code>assertThat</code> 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 <code>equals</code> d'Object. Le test réussit puisque la classe Biscuit définit une méthode <code>equals</code>.</p>
<p>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:</p>
<pre> assertThat("chocolate chips", theBiscuit.getChocolateChipCount(), equalTo(10));
assertThat("hazelnuts", theBiscuit.getHazelnutCount(), equalTo(3));</pre>
<h2>Autres frameworks de test</h2>
<p>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.</p>
<p>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 <a href="http://philippe.poumaroux.free.fr/index.php?tag/JMock%201">JMock 1</a> sont des matchers d'Hamcrest. Hamcrest fournit un adaptateur JMock 1 pour vous permettre d'utiliser les matchers Hamcrest dans vos tests JMock 1. <a href="http://philippe.poumaroux.free.fr/index.php?tag/JMock%202">JMock 2</a> 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 <a href="http://philippe.poumaroux.free.fr/index.php?tag/EasyMock">EasyMock</a> 2. De nouveau, voir les examples Hamcrest pour plus de détails.</p>
<h2>Un tour des matchers courants</h2>
<p>Hamcrest offre une bibliothèque de matchers utiles. En voici certains des plus importants.</p>
<ul>
<li>Noyau
<ul>
<li><strong>anything</strong> - correspond toujours, utile si vous ne vous souciez pas de l'objet qui est testé</li>
<li><strong>describedAs</strong> - décorateur pour ajouter une description d'échec personnalisé</li>
<li><strong>is</strong> - décorateur pour améliorer la lisibilité - voir "Sucre" ci-dessous</li>
</ul></li>
<li>Logique
<ul>
<li><strong>allOf</strong> - correspond si tous les matchers correspondent, fonctionne en court-circuit (arrêt à la première non correspondance, comme le && Java)</li>
<li><strong>anyOf</strong> - correspond si l'un des matchers correspond, fonctionne en court-circuit (arrêt à la première correspondance, comme le || Java)</li>
<li><strong>not</strong> - correspond si le matcher enrobé ne correspond pas et vice-versa</li>
</ul></li>
<li>Objet
<ul>
<li><strong>equalTo</strong> - test d'égalité des objets en utilisant Object.equals</li>
<li><strong>hasToString</strong> - teste Object.toString</li>
<li><strong>instanceOf</strong>, <strong>isCompatibleType</strong> - teste le type</li>
<li><strong>notNullValue</strong>, <strong>nullValue</strong> - teste pour null</li>
<li><strong>sameInstance</strong> - test l'identité d'objets</li>
</ul></li>
<li>Beans
<ul>
<li><strong>hasProperty</strong> - teste les propriétés JavaBeans</li>
</ul></li>
<li>Collections
<ul>
<li><strong>array</strong> - teste les éléments d'un tableau vis-à-vis d'un tableau de matchers</li>
<li><strong>hasEntry</strong>, <strong>hasKey</strong>, <strong>hasValue</strong> - teste si un map contient une entrée, une clef ou une valeur</li>
<li><strong>hasItem</strong>, <strong>hasItems</strong> - teste si une collection contient des éléments</li>
<li><strong>hasItemInArray</strong> - teste si un tableau contient un élément</li>
</ul></li>
<li>Nombre
<ul>
<li><strong>closeTo</strong> - teste si une valeur réelle est proche d'une valeur donnée</li>
<li><strong>greaterThan</strong>, <strong>greaterThanOrEqualTo</strong>, <strong>lessThan</strong>, <strong>lessThanOrEqualTo</strong> - teste l'ordre</li>
</ul></li>
<li>Texte
<ul>
<li><strong>equalToIgnoringCase</strong> - teste l'égalité de chaînes de caractères en ignorant la casse</li>
<li><strong>equalToIgnoringWhiteSpace</strong> - teste l'égalité de chaînes de caractères en ignorant les différences d'espace</li>
<li><strong>containsString</strong>, <strong>endsWith</strong>, <strong>startsWith</strong> - teste la correspondance de chaînes de caractères</li>
</ul></li>
</ul>
<h2>Sucre</h2>
<p>Hamcrest s'efforce de rendre vos tests aussi lisibles que possible. Par exemple, le matcher <code>is</code> est un enrobeur qui n'ajoute aucun comportement supplémentaire au matcher sous-jacent. Les affirmations suivantes sont toutes équivalentes:</p>
<pre> assertThat(theBiscuit, equalTo(myBiscuit));
assertThat(theBiscuit, is(equalTo(myBiscuit)));
assertThat(theBiscuit, is(myBiscuit));</pre>
<p>La dernière forme est autorisée puisque <code>is(T valeur)</code> est surchargé pour renvoyer <code>is(equalTo(valeur))</code>.</p>
<h2>Ecrire des matchers personnalisés</h2>
<p>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 !</p>
<p>Ecrivons notre propre matcher pour tester si une valeur réelle a la valeur NaN (<code>not a number</code>, n'est pas un nombre). Voici le test que nous voulons écrire:</p>
<pre> public void testSquareRootOfMinusOneIsNotANumber() {
assertThat(Math.sqrt(-1), is(notANumber()));
}</pre>
<p>Et voici l'implémentation:</p>
<pre> 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();
}
}</pre>
<p>La méthode <code>assertThat</code> 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 <code>TypeSafeMatcher</code> qui réalise le cast d'un Double pour nous. Nous n'avons besoin que d'implémenter la méthode <code>matchSafely</code> - qui vérifie simplement que le Double est un NaN - et la méthode <code>describeTo</code> - 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:</p>
<p><code>assertThat(1.0, is(notANumber()));</code></p>
<pre> fails with the message
java.lang.AssertionError:
Expected: is not a number
got : <1.0>}}</pre>
<p>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:</p>
<pre> 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()));
}
}</pre>
<p>Bien que la méthode <code>notANumber</code> 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.</p>
<h2>Génération du sucre</h2>
<p>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.</p>
<p>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 <code>org.hamcrest.Factory</code>. Par exemple:</p>
<pre> <matchers>
<!- - Hamcrest library - ->
<factory class="org.hamcrest.core.Is"/>
<!- - Custom extension - ->
<factory class="org.hamcrest.examples.tutorial.IsNotANumber"/>
</matchers></pre>
<p>Ensuite, exécutez l'outil en ligne de commande <code>org.hamcrest.generator.config.XmlConfigurator</code> 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:</p>
<pre> // 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();
}
}</pre>
<p>Enfin, nous pouvons mettre à jour notre test pour utiliser la nouvelle classe Matcher.</p>
<pre> 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()));
}
}</pre>
<p>Notez que nous utilisons maintenant le matcher is de la bibliothèque Hamcrest importé de notre propre classe de Matchers.</p>