mercredi 21 mars 2012

Générer un matcher Hamcrest unifié

Hamcrest propose une alternative avantageuse aux assertions de JUnit, en fournissant une bibliothèque de matchers ou prédicats pour l'écriture de règles de correspondance de façon déclarative : en cas d'échec du test, le message est beaucoup plus explicite. Une autre qualité, qui a conditionné la viabilité du projet Hamcrest, est son extensibilité : il est facile de développer ses propres prédicats et Hamcrest les prendra en charge. Pour un usage plus aisé, il est même possible de générer un matcher unique à partir d'une multitude de matchers spécialisés, ce qui évitera les imports statiques à répétition.

Rompu à l'utilisation de JUnit, je ne me suis pas laissé facilement convaincre par le recours aux assertions fondées sur Hamcrest plutôt qu'aux traditionnelles assertions natives de JUnit, même si HamcrestCore est désormais embarqué dans JUnit. Peut-être à cause de l'obligation de choisir un matcher adéquat en fonction du type de donnée à vérifier, et d'écrire l'import statique correspondant. En ce sens, j'aurais tendance à préférer dorénavant FEST-Assert, qui se présente comme un DSL (Domain-Specific Language), plus intuitif.

Mais pour en revenir au sujet de l'article, j'ai récemment mis en oeuvre le générateur de code d'Hamcrest, et comme j'ai constaté quelques imperfections, j'ai dû trouver différents remèdes.

1) Prérequis : définition d'un prédicat personnalisé

Un prédicat personnalisé est déclaré sous l'une de ces formes :
  • public class MyCustomMatcher extends org.hamcrest.BaseMatcher {
        ...
    }
  • public class MyCustomMatcher extends org.hamcrest.TypeSafeMatcher {
        ...
    }
  • public class MyCustomMatcher {
        public static org.hamcrest.Matcher factoryMethodName(final ObjectToBeMatchedClass expected) {
            return new org.hamcrest.BaseMatcher() {
                private ObjectToBeMatchedClass theExpected = expected;
                ...
            }
    }
2) Génération d'un prédicat unique

Le générateur s'appuie sur un fichier de configuration XML listant les différents prédicats personnalisés à regrouper. A l'exécution, le générateur identifie par introspection la fabrique (factory method) grâce à l'annotation @org.hamcrest.Factory, et génère une classe unique de façade pour l'utilisation de tous ces prédicats. La signature de la méthode annotée par @Factory doit impérativement être de la forme :
public static Matcher factoryMethodName(...)

NB : Sans cela, si la signature devait avoir un autre type de retour, comme celui du matcher personnalisé (par exemple : "public static MyCustomConcreteMatcher factoryMethodName(...)"), alors le type en argument ObjectToBeMatchedClass ne serait pas reporté dans le code généré et n'apparaîtrait plus dans le prédicat de façade. De cela découleraient des warnings à la compilation, notamment dans les classes qui utiliseraient le prédicat de façade.

Résultat en console :

Generating xapn.testing.hamcrest.generated.MyMatchers
                       [T] is(Matcher p0)
                       [T] is(T param1)
               [Object] is(Class p0)
[OrdinarySystem] ready()
[OrdinarySystem] powerful()
[OrdinarySystem] lessOrdinary(OrdinarySystem thanAnother)
[OrdinarySystem] moreOrdinary(OrdinarySystem thanAnother)

Remarque : Le type en argument apparaît dans les logs entre crochets. Si la signature de la fabrique n'est pas correctement formatée, cela apparaîtra dans les logs de cette manière :
                         [] factoryMethodName()
Pour remédier à cet avertissement minimaliste dans le cadre d'une chaîne de génération avec Maven, il pourrait être de bon ton de développer un Mojo qui renverrait une exception du type org.apache.maven.plugin.MojoExecutionException, voire une MojoFailureException (échec du build dans le cas d'une release par exemple).

3) Invocation du générateur dans une chaîne de génération industrialisée

Le générateur peut être très simplement appelé en ligne de commande :
java -cp hamcrest-all-1.1.jar org.hamcrest.generator.config.XmlConfigurator $config-file $source-dir $generated-class $output-dir

Voici les arguments attendus, tels qu'ils sont explicités en ligne de commande  :
Args: config-file source-dir generated-class output-dir
config-file : Path to config file listing matchers to generate sugar for.
                  e.g. path/to/matchers.xml
source-dir  : Path to Java source containing matchers to generate sugar for.
                  May contain multiple paths, separated by commas.
                  e.g. src/java,src/more-java
generated-class : Full name of class to generate.
                  e.g. org.myproject.MyMatchers
output-dir : Where to output generated code (package subdirs will be
                  automatically created).
                  e.g. build/generated-code

Il faudra toutefois avoir déjà compilé les différents matchers listés dans $config-file, puisque le générateur est en fait alimenté par leurs binaires.

Hamcrest est aujourd'hui encore construit par Ant, aussi ce ne sera pas un problème d'ajouter une tâche Ant dans le processus de build d'un projet, afin de générer le code source du prédicat de façade et de le compiler ensuite. Cela donnerait une tâche Ant comme suit pour invoquer le générateur Hamcrest, suivi d'une tâche Ant pour compiler le code source généré :
<java classname="org.hamcrest.generator.config.XmlConfigurator"
    fork="true"
    failonerror="true"
    maxmemory="128m"
    >
  <arg value="${config-file} ${source-dir} ${generated-class} ${output-dir}"/>
  <classpath>
    <pathelement location="lib/hamcrest-all-1.1.jar"/>
    <pathelement path="${java.class.path}"/>
  </classpath>
</java>

Avec Maven, c'est encore plus simple, grâce au plugin org.codehaus.mojo:exec-maven-plugin. Si l’artéfact org.hamcrest:hamcrest-all fait partie des dépendances, toujours en ligne de commande cela donne :
mvn exec:java -Dexec.mainClass="org.hamcrest.generator.config.XmlConfigurator" -Dexec.args="$config-file $source-dir $generated-class $output-dir"

En revanche, dans le cadre d'un projet multimodule Maven, la démarche est un peu plus subtile. Typiquement, vous avez écrit vos matchers personnalisés dans un module dédié, et vous souhaitez pouvoir les utiliser dans vos tests dans des modules qui constituent votre projet. Pour obtenir une bibliothèque de matchers personnalisés, vous créez un module de type jar, et tirez une dépendance vers hamcrest-all, de scope compile. Dans les autres modules de votre projet, vous pourrez dorénavant dépendre de votre bibliothèque pour vos test, avec le scope test, de même qu'avec JUnit, TestNG ou Hamcrest. Rien de compliqué jusqu'à présent, seulement du classique.
Pour générer votre bibliothèque de matchers, vous allez en revanche devoir reproduire la démarche présentée avec Ant :
  1. compilation des matchers personnalisés,
  2. invocation du générateur Hamcrest,
  3. compilation du matcher de façade généré.
C'est là que ça devient problématique, étant donné que le cycle de vie Maven spécifique du type de package jar suit les phases suivantes dans cet ordre, sans qu'il soit possible de les répéter dans le même processus de build :
  • process-resources
  • compile : compilation des matchers personnalisés, à laquelle on ajoute l'invocation du générateur.
  • process-test-resources
  • test-compile
  • test
  • package
  • install
  • deploy
En l'état, le matcher de façade ne sera pas embarqué dans le JAR de l'artéfact final, faute d'avoir été compilé. Il faut donc choisir une solution parmi ces alternatives :
  • soit on développe un nouveau cycle de vie personnalisé : ça revient un peu à tuer une mouche avec un canon ;
  • soit on génère le matcher de façade dans un autre module : simple, expéditif, mais pas très élégant, car j'aurais voulu que tous les matchers soient empaquetés dans le même JAR pour faciliter la distribution de l'artéfact ;
  • soit on s'arrange pour tout faire rentrer dans la phase compile du processus de build de l'artéfact.
Sur internet, j'ai pu lire des posts où il était question de développer un Mojo, mais le problème lié au cycle de vie reste le même. Pour ma part, j'ai réussi à mettre en oeuvre la troisième option, en créant un profil Maven "hamcrest". Voici la définition de ce profil :
  • Profil désactivé par défaut.
  • Activation du profil si le code source à générer est absent.
  • Propriétés : arguments à fournir au générateur de code Hamcrest.
  • Plugin maven-clean-plugin / goal clean, attaché à la phase clean, pour supprimer le code source généré (dans le cycle de vie Clean de Maven).
  • Plugin maven-antrun-plugin / goal run, attaché à la phase compile, pour afficher un message echo en console contenant la valeur des arguments à destination du générateur de code Hamcrest.
  • Plugin exec-maven-plugin / goal java, attaché à la phase compile, pour invoquer le générateur de code Hamcrest.
  • Plugin maven-compiler-plugin / goal compile, attaché à la phase compile, pour forcer la compilation du code source généré.
Dès lors :
  • mvn clean install, pour laisser Maven activer automatiquement le profil "hamcrest".
  • mvn clean install -Phamcrest, pour forcer l'activation du profil "hamcrest".
Pour ne pas surcharger le contenu de cet article, je n'ai pas inclus le code source des matchers, ni les POM du projet de démonstration. Vous pouvez retrouver ce contenu dans son intégralité sur mon dépôt GitHub UnitTesting4Java : la bibliothèque des matchers personnalisée est "hamcrest-sugar-generation", et le matcher unifié est utilisé dans un test du module "junit4-features", dans le package "xapn.testing.junit.hamcrest.sugarmatcher".

4) Quelques conseils et astuces

Matcher Is :
Pour des raisons de commodité, pensez à toujours ajouter le matcher org.hamcrest.core.Is dans le fichier de configuration XML du générateur de code d'Hamcrest. Comme son utilisation est fréquente, cela vous épargnera d'ajouter un import statique dans tous vos tests pour un seul prédicat.
<matchers>

    <!-- Hamcrest library -->
    <factory class="org.hamcrest.core.Is"/>

</matchers>

Pourquoi un profil ?
Un profil peut comporter des clauses d'activation, ce qui, dans ce cas, évitera de lancer inutilement une génération à chaque processus de build. Par contre, il faudra penser à activer le profil suite à une modification des matchers personnalisés.

Quelle version d'Hamcrest ?
La version d'Hamcrest embarquée dans JUnit n'est pas la dernière, il y a même longtemps qu'elle n'a pas été mise à jour. Pour bénéficier de toute la variété des matchers développés par Hamcrest, il peut être utile d'ajouter la bibliothèque HamcrestAll dans ses dépendances.

Faciliter l'écriture des imports statiques dans une classe :
L'IDE Eclipse permet d'organiser automatiquement les imports statiques par autocomplétion, ou encore grâce au raccourci Ctrl+Shift+M. Pour que cela fonctionne également avec les matchers d'Hamcrest, il convient de modifier les préférences d'Eclipse : dans Java / Editor / Content Assist / Favorites, vous pouvez ajouter soit de nouveaux types ("New Type"), soit de nouveaux membres ("New Member").
Exemples de types intéressants à ajouter aux imports statiques automatiques :
  • org.hamcrest.CoreMatchers.* (hamcrest-core ou junit) : prédicats de base fournis par JUnit et issus d'HamcrestCore.
  • org.hamcrest.Matchers.* (hamcrest-all) : ces prédicats fournis par HamcrestAll réunissent les prédicats de base de HamcrestCore, également embarqués par JUnit, ainsi que des prédicats dédiés aux collections, aux textes, aux nombres et d'autres encore.
  • org.junit.matchers.JUnitMatchers.* (junit) : le prédicat de façade de JUnit agrégeant les prédicats internes de JUnit, ainsi que les prédicats d'Hamcrest embarqués par JUnit.
Hamcrest : org.hamcrest.CoreMatchers vs org.hamcrest.Matchers ?
Qui peut le plus peut le moins, et dans ce cas, Matchers recouvre CoreMatchers.

Contribution :
Hamcrest est aujourd'hui maintenu sous Google Code, et toute contribution devrait passer par la soumission d'un patch, à attacher à une issue existante... La question de faire passer le projet sous GitHub a été soulevée.

Liens :

mercredi 14 mars 2012

Techniques pour amener les équipes à l’excellence

Introduction

J'ai assisté mardi soir à la conférence du même nom animée à Paris par Michel Goldenberg. Le sujet principal était la maîtrise du degré de motivation d'une équipe de développement au cours du cycle de vie d'un produit logiciel. Si Scrum propose un cadre pour gérer le suivi d'un projet, il ne répond pas à la question des moyens dont peuvent disposer un Scrum Master et une équipe pour agir sur la motivation et les performances.

A l'opposé d'une attitude managériale du type "Command & Control", le manifeste agile privilégie l'auto-organisation des équipes et la responsabilisation de leurs membres : l'objectif commun est de fournir un logiciel qui fonctionne et utile pour les utilisateurs en respectant les délais. Cela part d'un bon sentiment, l'objectif est lui-même louable, malheureusement peut-on dire que le concept tiendra en haleine la motivation des développeurs, après des mois, voire des années à suivre le cérémonial de Scrum ? En pratique, que constate-on ? Et si la démotivation doit à terme gagner les esprits comme c'est le cas dans toutes les équipes de par le monde, comment peut-on y remédier ?

Je vais donc essayer de répondre à ces questions en quelques articles, en reprenant les informations échangées lors de cette conférence.

dimanche 19 février 2012

[Maven] Comprendre les fonctionnalités d'un plugin

Il n'existe pas de liste exhaustives des plugins Maven disponibles. En revanche, à partir du nom d'un plugin, il est possible d'obtenir sa documentation, et ce grâce au plugin dédié help.

Le plugin help propose entre autres mojos describe, qui fournit une description de n'importe quel plugin, ainsi que la liste des mojos d'un plugin et l'explication de la tâche qu'ils accomplissent.
mvn help:describle -Dplugin=<nom du plugin>

Appliquons par exemple le mojo help:describe sur lui-même :
mvn help:describe -Dplugin=help

Pour davantage d'informations, notamment sur les paramètres acceptés par chaque mojo :
mvn help:describe -Dplugin=help -Ddetail
ou encore : mvn help:describe -Dplugin=help -Dfull

Un mojo très intéressant du plugin help est help:effective-pom (Cf. la documentation fournie par les commandes précédentes).

[Maven] Plugin & Mojo

Maven est un moteur dont le coeur définit le cycle de vie pour la construction d'un projet logiciel : traitement des ressources, compilation du code source, traitement des binaires, traitement des ressources de test, compilation des tests, passage des tests, packaging.
Toutes les étapes de génération ou de traitement prises en charge par Maven sont en fait réalisées par les plugins de Maven. Le cycle de vie par défaut proposé par Maven consiste en un ensemble ordonné de phases, auxquelles sont attachés un ou plusieurs plugins.

Un plugin Maven est lui-même constitué de tâches programmées (ou goals), et ce sont en fait ces tâches qui sont attachées aux phases du cycle de vie. S'agissant de Maven, on parle plus communément de Mojo : Maven Old Java Object.

Ainsi, lorsqu'on demande à Maven de construire un projet, Maven déroule son cycle de vie, et à chaque phase rencontrée, Maven lance certains Mojos du ou des plugins associés à cette phase.

En définitive, les fonctionnalités de Maven peuvent être étendues à l'infini par le développement et l'utilisation de nouveaux plugins, susceptibles d'enrichir le cycle de vie par défaut, voire de définir des cycles de vie personnalisés.

Liens :

jeudi 2 février 2012

[Git] Mettre du contenu de côté

... ou comment sauvegarder son travail en cours dans la pile temporaire.

Dans certaines situations où un travail n'est pas terminé ni à jeter et où l'on souhaite le mettre de côté en attendant d'y revenir, le temps de travailler sur autre chose, Git propose d'utiliser une pile dédiée (FiFo) grâce à git stash. Cette commande permet de sauvegarder sans commit, et de revenir au dernier commit de la branche courante (de façon équivalente à git reset --hard HEAD). Au moment de la récupération, Git offre plusieurs possibilités :
  • Voir les éléments de la pile : git stash list
  • Voir les modifications sauvegardées dans un élément de la pile dans la branche courante : git stash show stash@{number}, pour voir les modifications fichier par fichier, et avec l'option -p pour voir les modifications ligne par ligne dans chaque fichier. number=0 désigne le dernier élément, number=1 l'avant-dernier, number=2 l'avant-avant-dernier, etc.
  • Sauvegarder l'index dans la pile, en y joignant un message personnalisé (utile pour se souvenir du contenu sauvegardé) : git stash save "message"
  • Récupérer le dernier élément de la pile dans la branche courante : git stash apply
  • Récupérer un élément précis de la pile dans la branche courante : git stash apply stash@{number}.
  • Créer une branche dans laquelle récupérer le dernier élément de la pile : git stash branch new_branch_name
  • Supprimer un élément de la pile : git stash drop stash@{number}