Tests unitaires sous Maven

Tester les applications avec Maven

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Maven permet, comme Apache Ant, d'exécuter les tests unitaires et de consulter ses résultats sous un format souhaité (fichier ou output directemenet dans la console). Cependant, quelques problèmes peuvent se produire lors du transfert des tests sous Maven. On les découvrira à travers cet article.

Comme on l'a indiqué dans l'article consacré au transfert d'un projet sur Maven, Maven impose une structure propre à la suite des tests automatiques. Juste au titre d'un bref rappel, les tests doivent se situer dans le répertoire /src/test et reprendre la même structure que les classes utilisées par l'application. On aura donc un tel sous-repértoire des tests :

   |---test
   |-----java
   |-------com
   |---------example
   |-----------library
   |-------------model
   |---------------TestEntity.java
   |-------------AbstractControllerTest.java
   |-------------BookingControllerTest.java
   |-------------NewsletterSenderAsyncTest.java
   |-------------SpelTest.java
   |-------------SubscriptionControllerTest.java
   |-------------SubscriptionTest.java
   |-------------SuggestionControllerTest.java
   |-------------ToolTest.java
   |-------------XSSCleanerTest.java
   |-----resources
   |-------test-config.xml

Exécuter les tests unitaires sous Maven

Pour lancer les tests unitaires sous Maven il faut utiliser la commande mvn clean test. En fait, elle fait appel à des méthodes. La première fonction supprime tous les fichiers générés et les recompile une nouvelle fois. La deuxième lance les tests spécifés dans notre dossier /src/test/java/com/example/library.

Après l'exécution de cette commande, on verra sur l'écran les erreurs qu'on résoudra plus loin dans l'article.

Mauvais package JavaEE sous Maven

La première exécution du test retournera une erreur du type :


	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:68)
testValidBooking(com.example.library.test.BookingControllerTest)  Time elapsed: 1.312 sec  <<< ERROR!
java.lang.ClassFormatError: Absent Code attribute in method that is not native or abstract in class file javax/persistence/PersistenceContextType
	at java.lang.ClassLoader.defineClass1(Native Method)

Il est provoqué par le fait que le JAR avec JavaEE, chargé initialement depuis Maven (on suppose que <scope>provided</scope> est absent dans le pom.xml pour les dépendances). Et ce package n'est qu'un API qui contient les noms des classes et interfaces sans leurs contenus.

Pour rémedier à ce souci, il suffit de télécharger l'archive JavaEE sur le site du Java et l'installer manuellement dans le répertoire local du Maven. L'installation dans le répertoire local s'effectue grâce à la commande mvn install:install-file -Dfile=./javaee-16.jar -DgroupId=javax -DartifactId=javaee-api -Dversion=16 -Dpackaging=jar. Les attributs groupId, arrtifactId et version servent à déterminer quelle dépendance doit être récupérée dans pom.xml.

Exclure les classes des tests sous Maven

Parfois on veut exclure certaines classes de notre suite des tests. En occurrence, on ne voudrait pas que la classe de configuration, AbstractControllerTest, soit lancée. Le lancement provoquera un message d'erreur :

Running com.example.library.test.AbstractControllerTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.074 sec <<< FAILURE!
initializationError(com.example.library.test.AbstractControllerTest)  Time elapsed: 0.024 sec  <<< ERROR!
java.lang.Exception: No runnable methods
        at org.junit.runners.BlockJUnit4ClassRunner.validateInstanceMethods(BlockJUnit4ClassRunner.java:171)
        at org.junit.runners.BlockJUnit4ClassRunner.collectInitializationErrors(BlockJUnit4ClassRunner.java:115)
        at org.junit.runners.ParentRunner.validate(ParentRunner.java:269)
        at org.junit.runners.ParentRunner.<init>(ParentRunner.java:66)
        at org.junit.runners.BlockJUnit4ClassRunner.<init>(BlockJUnit4ClassRunner.java:59)
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.<init>(SpringJUnit4ClassRunner.java:104)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
        at org.junit.internal.builders.AnnotatedBuilder.buildRunner(AnnotatedBuilder.java:31)
        at org.junit.internal.builders.AnnotatedBuilder.runnerForClass(AnnotatedBuilder.java:24)
        at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:57)
        at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:29)
        at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:57)
        at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:24)
        at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:262)
        at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:153)
        at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:124)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray2(ReflectionUtils.java:208)
        at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:159)
        at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:87)
        at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:153)
        at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:95)

Dans ce cas il faut donc exclure la classe dans le plugin qui s'occupe de lancer les tests sous Maven. Le plugin s'appelle Maven Surefire Plugin. Il exécute les tests mais aussi se charge de générer les résultats, soit sous forme des fichiers texte, soit directement dans la console. Les résultats sont placés dans le répertoire /target/surefire-reports.

Regardons maintenant comment configurer ce plugin pour qu'il n'exécute pas de tests pour la classe AbstractControllerTest :

  <build>
    <finalName>LibrarySpringSample</finalName>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.14.1</version>
        <configuration>
          <excludes>
            <exclude>**/AbstractControllerTest.java</exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

La configuration est assez explicite. La balise <exclude /> peut contenir des enfants-balises <exclude /> qui déterminent les classes à exclure des tests. Les classes peuvent être déterminées directement ou via une expressions régulière. Plus d'informations à ce sujet se trouve dans la documentation du Maven Surfire.

Maven est un outil puissant qui permet de piloter un projet, aussi bien ses dépendances que ses tests. Dans cet article on a présenté comment implémenter les tests unitaires JUnit sous Maven et à quels problèmes on peut faire face pendant le processus.

Bartosz KONIECZNY Maven

Une question ? Une remarque ?

*

*

Un conseil Symfony2

Problème avec les valeurs par défaut pour un champ type checkbox ?

La gestion des formulaires sous Symfony2 correspond parfaitement au slogan de jQuery "write less, do more". En effet, il suffit de déterminer les validateurs une fois et après seulement les adapter en fonction du groupe du formulaire. Egalement la définition des champs est très intuitive. La récupération des données saisies, en cas d'une erreur de validation, est aussi automatique. Cependant, il se peut que vous renctonreriez un problème avec les champs du type checkbox. Supposons, qu'on veut construire un checkbox qui prendra pour valeur des chiffres (integers) correspondant aux identifiants aux apparements recherchés (1 pièce, 2 pièces, 3 pièces etc.) :

protected static $types = array(1 => '1 room', 2 => '2 rooms', 3 => '3 rooms');
Après une validation incorrecte, on retourne sur la page avec le formulaire pré-rempli. Pour être sûr que la liste des checkboxes va avoir les champs pré-cochés (par exemple 1 room et 3 rooms), il faut s'assurer que les types des valeurs sauvegardées sont égales à celles de la variable statique $types. Souvent il faut faire une boucle pour régler un éventuel problème avec les valeurs pré-cochés dans multiple checkboxes sous Symfony2 :
    foreach($values as $v => $val)
    {
      $vals[] = (int)$val;
    }