Accueil Nos publications Blog Du bon usage de JUnit 1/2

Du bon usage de JUnit 1/2

frog_shit_junitPour rester dans la ligne de l’excellent post de Bruno Doolaeghe, je vous propose un tutoriel sur JUnit. Même si TestNG est de plus en plus populaire, on n’a pas besoin de faire un sondage pour savoir que JUnit est toujours le framework de tests le plus utilisé dans le monde Java. Rien que pour cette raison, il mérite d’être étudié avec soin. C’est la raison d’être de ce tutoriel.

Je ne souhaitais pas réaliser une liste séquentielle des fonctionnalités de JUnit, car en l’état ça n’aurait pas grande utilité. En effet, JUnit est un projet relativement ancien et au fil des releases, plusieurs solutions successives ont été proposées pour répondre aux mêmes problèmes. J’ai donc établi une liste de besoins récurrents dans l’écriture des tests unitaires indépendamment du framework. A chaque fois, je présenterai d’abord le besoin, ensuite je montrerai comment JUnit répond à ce besoin. Mon objectif n’est pas de comparer JUnit à ses challengers. Je me contenterai donc de souligner les atouts ou les faiblesses des solutions proposées par JUnit sans préciser ce qui se fait dans d’autres frameworks.

Je tenais à rédiger un tutoriel accessible à tous, même aux débutants. C’est la raison d’être de la première section “Pré-requis”. Dans cette section, j’ai d’abord précisé quelques conventions de coding, créé un premier test puis introduit la notion de runner. Cette première section correspond, à mon sens, à l’usage minimal de JUnit. Si vous n’êtes donc pas un vrai débutant, vous pouvez sauter cette section. Enfin, s’agissant du cas particulier des Theory, il s’agit non seulement d’une nouvelle fonctionnalité offerte par JUnit mais aussi d’une nouvelle notion. C’est pour cette raison que j’ai rédigé un article séparé, consacré à ce sujet en faisant abstraction du framework de test. Si la notion de Theory ne vous est pas familière, je vous conseille de lire ce post d’abord.

Prérequis

A César…

JUnit a été créé par Kent Beck et Erich Gamma.

Installation de JUnit

L’installation de JUnit est relativement simple. Il suffit de déclarer une dépendance à Junit dans votre gestionnaire de build. Par exemple, si vous utilisez Maven, rajoutez ceci à votre POM.

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>

Si vous préférez le faire à l’ancienne, vous pourrez toujours récupérer les binaires ici.

Conventions de coding

Les tests, et donc les tests unitaires, sont une spécification fonctionnelle du comportement de votre code. Bien souvent d’ailleurs, longtemps après que la documentation fonctionnelle soit devenue obsolète, un test bien écrit sera l’ultime spécification. C’est pour cela que la lisibilité (pour un humain) du test doit être une préoccupation centrale dans l’écriture des tests. Les conventions que je présente ci-dessous ont donc pour unique objectif de permettre à un humain de comprendre ce que fait mon test le plus rapidement possible sans avoir à déboguer la classe testée.

Structure des tests

Il peut être intéressant de séparer les différentes parties du test. Pour ma part, j’ai une préférence pour le triptyque Given-When-Then que je réutiliserai dans mes exemples. Cette structure est identique à la division Arrange Act Assert. En français, ça donne :

Etant donné (Given):
* des paramètres
* les valeurs que j’espère avoir en retour

Quand j’effectue une action donnée (When),
* appel de la méthode testée

Alors, je m’attends à ce que (Then) :
* Les assertions suivantes soient vérifiées

De mon point de vue, cette organisation des tests fait aujourd’hui partie des bonnes pratiques et devrait être adoptée de facto. En effet :

  • cette division facilite la lecture et donc la maintenance des tests
  • cette division oblige le développeur à penser « fonctionnel » et à se poser clairement la question de ce qui doit être testé. On peut ainsi identifier rapidement les « happy tests » (Lasse Koskela dans “Effective Unit Testing”), ces tests qui ne testent rien car ne comportant aucune assertion (bloc then absent).

Nomenclature

Il y a deux moments où le nom de la méthode de test a de l’importance :

  • pendant la phase de maintenance
  • quand le test a échoué .

Dans ces deux cas, le nom de la méthode est un élément de communication et plus ce nom sera explicite meilleure sera la communication.

C’est pour cette raison que nous préconisons des noms suffisamment explicites sans tenir compte de leur longueur. Dans le même ordre d’idée, nous avons opté pour le caractère “_” comme séparateur de mots, afin de pouvoir avoir un nom qui ressemble le plus possible à une phrase. Ainsi, à la seule lecture du nom, on peut savoir la nature du test.

Le fait de rajouter un séparateur de mots, le “_”, et donc de ne pas respecter les conventions pour le nommage des méthodes de test choque beaucoup de personnes. Cependant, si les conventions de nomenclature ont un sens dans le code “normal”, elles sont contre-productives dans le cas du nom des méthodes de test.

Pourquoi utiliser un framework de test ?

Techniquement, rien n’empêche d’écrire un bon test sans utiliser de framework de test. Nous l’avons tous fait au moins une fois : il suffit de rajouter un static void main . Les frameworks de tests et en particulier JUnit vont nous apporter de la souplesse dans nos tests :
* gestion des tests qui échouent : le framework de test n’arrête pas l’exécution des tests des que l’un d’entre eux est en échec.
* le reporting : le framework de tests va fournir un rapport d’exécution avec la liste des tests en échec et la stacktrace correspondantes. Ce rapport pourra être interprété par notre IDE.

Les frameworks de test offrent davantage de fonctionnalités que nous aborderons plus loin.

Par ailleurs, à l’ère de l’intégration continue, nous devons être capable de déléguer l’exécution de nos tests à un gestionnaire de build (maven par exemple) lui même piloté par un outil tiers (Jenkins par exemple). L’usage d’un framework de tests est devenu indispensable.

Notion de runner

Le runner, c’est la classe qui va identifier les classes et les méthodes de test pour déterminer quels tests exécuter. C’est donc cette classe qui doit interpréter les annotations JUnit présentes sur la classe de test telle que @Test, @Ignore (pour ignorer un test).

Un runner, c’est une classe qui étend org.junit.runner.Runner. En pratique, pour utiliser un runner, on utilise l’annotation @RunWith(nom_du_runner.class). En général, les runner sont suffixés Runner mais nous verrons plus loin que ce n’est pas toujours respecté. Par défaut, c’est le runner BlockJUnit4ClassRunner qui est appelé. C’est pour cette raison qu’on n’a pas besoin de le déclarer si on écrit un test “ordinaire”.

Dans la suite, nous verrons que parfois le runner va souvent changer. En effet, en fonction du type de test (et donc des annotations), le runner n’a pas le même comportement.

Exécution d’un test JUnit

Il existe diverses manières de lancer un test JUnit. Cependant, quelle que soit la méthode choisie, ça reviendra toujours à lancer un runner JUnit en lui passant en argument une liste de tests à exécuter.

La plupart de nos IDE (Eclipse, IntelliJ et Netbeans) incorporent un runner graphique qui est appelé lorsque nous lançons nos tests depuis l’IDE.

Lorsque vous utilisez un gestionnaire de build, vous pouvez lui déléguer l’exécution des tests. Dans notre cas, nous utilisons Maven et donc nous pouvons lancer nos tests à partir du dossier du projet, grâce à la commande mvn test. Cette commande va exécuter par défaut les tests présents dans le répertoire src/test/java

Les sources des exemples

Tous les exemples de code de cet article sont sur Github. Le projet est organisé comme suit :

  • Dans \src\test\java\com\ptngaye\junittutoriel\groups, on a les exemples liés à la fonctionnalité Regroupement des tests
  • Dans \src\test\java\com\ptngaye\junittutoriel\dataseries, on a les exemples liés à la fonctionnalité Gestion des jeux de données
  • Dans \src\test\java\com\ptngaye\junittutoriel\interceptors, on a les exemples liés à la fonctionnalité Intercepteurs
  • Dans \src\test\java\org se trouvent les sources de la librairie ClasspathSuite qui n’est pas présente dans sa dernière version sur un repository maven public.

J’ai utilisé le JDK 1.8, parce que c’est le dernier, que c’est gratuit 🙂 et surtout je voulais utiliser la nouvelle API de date. Cependant, la version de Java n’a rien à voir avec les fonctionnalités de JUnit présentées ci-dessous. Normalement, si j’en crois le POM de JUnit, la version 1.5 du JDK (annotation) devrait suffire.

Enfin, s’agissant de JUnit, j’ai utilisé la 4.11 et il faudra obligatoirement cette version.

Un premier test JUnit

Un test JUnit c’est une méthode “public”, sans argument, ne renvoyant rien (void) et annotée @Test. Techniquement, la méthode suivante est donc un test même si, en vérité, elle ne teste rien du tout.

import org.junit.Test;

public class MyClassTest {
    @Test
    public void testNothing() {
    }

Nous allons maintenant écrire un “vrai test”. Imaginons que j’ai une classe HolidaysHelper avec une méthode identifyEasterDay qui, pour une année fournie en paramètre, me retourne la date de Pâques. Voici un test qui vérifie que pour 2014, le dimanche de Pâques est à la date du 20 avril.


import org.junit.Test;

import java.time.LocalDate;
import java.time.Month;

import static org.junit.Assert.assertEquals;

public class HolidaysHelperTest {
    private HolidaysHelper holidaysHelper = new HolidaysHelper();

    @Test
    public void test_easterDay_in_2014_is_20_april_2014() throws Exception {
        //Given
        int year = 2014;
        LocalDate easterDayExpected = LocalDate.of(2014, Month.APRIL, 20);

        //When
        LocalDate easterDayActual = holidaysHelper.identifyEasterDay(year);

        //Then
        assertEquals(easterDayExpected, easterDayActual);
    }
}

Si nous modifions le test en mettant le 17 Mars à la place du 20 Avril, alors une exception de type AssertionError sera levée.

Le code couleur des statuts d’exécution

Au niveau des IDE, il existe un code pour distinguer le statut d’un test :

  • Vert : le test s’est déroulé sans aucune erreur et toutes les assertions ont été vérifiées
  • Rouge : une exception imprévue a été levée. Il faut distinguer ce type d’erreur des assertions qui échouent
  • Une couleur autre que Vert et Rouge, par exemple bleue pour Eclipse : une assertion n’a pas été vérifiée

Pour résumer le bleu indique une erreur métier, puisque la spécification n’est pas respectée tandis que le rouge indique un défaut technique inhérent au test.

A cette étape, nous savons écrire un test unitaire. Nous pourrions arrêter notre apprentissage de JUnit ici, ça ne nous empêcherait pas d’écrire des tests robustes. C’est ce que j’ai aussi fait pendant longtemps. Je me suis contenté de l’annotation @Test. Cependant, comme nous allons le voir dans la suite, JUnit peut nous aider davantage dans nos tests, en fournissant des solutions éprouvées à des problèmes récurrents.

Les mocks et les assertions

Avant d’aller plus loin, je voudrais aborder deux deux préoccupations sont transversales : les mock et les assertions. On devrait pouvoir choisir son framework de mock et sa librairie d’assertion indépendamment de son framework de test. Néanmoins, puisque c’est un tutoriel, je vais quand même donner une indication subjective.

Les mocks

Il est souvent nécessaire de simuler le comportement d’une dépendance lorsqu’on teste un objet. Concrètement, si un objet A utilise un objet B, alors on peut vouloir tester A en supposant que B a un certain comportement. Les frameworks de mock sont des outils qui vont nous permettre de réaliser ce type de use case (entre autres). Chacun de ces outils vient avec ses annotations et donc son runner. Il faudra donc ne pas oublier d’activer ce runner (@Runwith) sinon nos tests échouerons.

La plupart des frameworks de gestion de mocks fonctionnent avec JUnit. Il sera cependant judicieux dans le choix du framework de tenir compte de la lisibilité de la syntaxe proposée. Dans ce sens, l’excellent JMockit ou encore le très populaire tandem Mockito / PowerMock me semblent de très bons candidats.

Les assertions

Dans la structure présentée ci-dessus, les assertions correspondent au bloc “then”. Elles pourraient être effectuées sans avoir à intégrer de librairie additionnelle. Par exemple :

//Then
if(!easterDayExpected.equals(easterDayActual)){
  throw new Exception("Le jour de pâques en 2014 n'est pas correct !!! Trouvé :" + easterDayActual + "  Attendu: easterDayExpected");
}

En théorie, on pourrait donc se baser des API d’assertion (ou Matcher mais je préfère le terme assertion). De plus, même si on ne fait pas nos “assertions maison” comme ci-dessus, toutes les assertions pourraient être écrites à l’aide du assertEquals de JUnit.

Cependant, il est conseillé d’utiliser une bibliothèque d’assertion car :

  • les assertions natives de JUnit sont trop limitées (même avec la nouvelle méthode assertThat)
  • les bibliothèques d’assertion peuvent nous permettre d’être plus productifs en réutilisant du code existant plutôt que d’écrire nos propres méthodes d’assertion

Enfin, il faut garder à l’esprit que le plus important, c’est la lisibilité et donc choisir une bibliothèque offrant la syntaxe la plus proche possible du langage naturel. Dans ce sens, même hamcrest est intéressante à bien d’égards, même si la librairie assertJ (fork de Fest-Assert ) a ma préférence.

Regroupement des tests

Le besoin

  • Lancement en bloc d’un ensemble de tests
  • Ignorer certains tests à certains moments

Cas d’étude
Nous allons reprendre le use case décrit dans la documentation officielle de JUnit. Supposons que nous ayons des tests qui sont très lents à s’exécuter et d’autres qui sont plus rapides. On sait que si les tests sont trop chronophages, le développeur aura tendance à les skiper en totalité. Le juste milieu (à part améliorer la performance des tests) pourrait consister à dire : “Par défaut, quand le développeur effectue un build, seuls les tests rapides sont lancés. Mais quand on est sur le serveur d’intégration continue, tous les tests, sans exception, doivent être lancés.”

Supposons que nous ayons deux classes de tests ProductDaoTest, CustomerDaoTest qui définissent chacune un test de l’insertion en base de données. Ces deux classes définissent en plus une méthode permettant de tester la génération de la clé de la ligne à insérer (produit ou client). Les méthodes de test de l’insertion sont plus lentes car elles nécessitent une base mémoire, tandis que la génération de la clé primaire ne nécessite aucune ressource. Nous souhaitons regrouper les tests les moins rapides d’une part (les insertions) et les tests les plus rapides d’autre part.

La classe ProductDaoTest

import org.junit.Test;

public class ProductDaoTest {
    @Test
    public void should_insert_new_row() {
        //Given

        //When

        //Then
        System.out.println("ProductDaoTest.should_insert_new_row");
    }

    @Test
    public void should_build_businessKey() {
        //Given

        //When

        //Then
        System.out.println("ProductDaoTest.should_build_businessKey");
    }
} 

La classe CustomerDaoTest

import org.junit.Test;

public class CustomerDaoTest {
    @Test
    public void should_insert_new_row() {
        //Given

        //When

        //Then
        System.out.println("CustomerDaoTest.should_insert_new_row");
    }

    @Test
    public void should_build_businessKey() {
        //Given

        //When

        //Then
        System.out.println("CustomerDaoTest.should_build_businessKey");
    }
}

La solution JUnit

Les suites

Les suites sont historiquement la première réponse apportée par JUnit pour regrouper des tests. Une suite déclare une liste de classes de tests et quand on l’exécute, tous les tests des classes correspondantes sont exécutés.

En pratique, une suite est une classe de test vide avec les annotations @Suite et @RunWith(Suite.class). Dans notre cas, nous pourrions créer une suite FastTestsTestSuite comme suit :

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
        ProductDaoTest.class,
        CustomerDaoTest.class,
})
public class FastTestsTestSuite {
}

Notez l’annotation @RunWith(Suite.class) au dessus de la classe. En effet, la classe Suite n’est pas uniquement l’annotation permettant de définir une suite, mais également le runner en charge des suites. Nous verrons dans les prochains exemples que les runner natifs de JUnit ne sont pas toujours suffixés “runner”.

On peut le voir, le soucis avec cette suite, FastTestsTestSuite, c’est la granularité. Une fois qu’elle intègre une classe de tests, elle englobe tous les tests de la classe. Or, ce que nous voulons, c’est prendre une partie des tests de chaque classe. Le seul moyen d’arriver à nos fins avec les suites, c’est de splitter nos classes de tests pour avoir un ProductDaoFastTest et un ProductDaoSlowTest par exemple. Au delà du désagrément d’avoir à modifier la structure de nos tests, les suites ont un défaut intrinsèque : il faut maintenir dans la suite la liste de tous les tests qui la composent. On peut imaginer que cela reviendrait à avoir une liste de plusieurs dizaines ou centaines de tests à déclarer dans FastTestSuite… Ce type de solution est maintenant obsolète et je n’en parlerai donc pas davantage.

C’est pour apporter davantage de flexibilité dans le regroupement des tests que les catégories ont été créées.

Les catégories

Les catégories permettent de regrouper les tests selon un critère autre que la classe Java. Il faut voir la catégorie comme un flag placé sur un test. Cela est mis en oeuvre grâce à l’annotation @Category qui peut être placée :

  • soit sur la classe de test et dans ce cas tous les tests sont inclus dans ladite catégorie. Dans ce cas, la catégorie est superflue puisqu’une suite ferait l’affaire.
  • soit sur une méthode de test et dans ce cas, ladite méthode est incluse dans la catégorie.

Définition des catégories

Dans notre cas, il nous faut créer deux catégories :

  • une catégorie par défaut pour les tests DAO supposés lents: SlowTestCategory
  • une catégorie pour les tests rapides : FastTestCategory.

Pour JUnit, une catégorie, c’est une interface (ou une classe) vide. Nous allons donc créer deux interfaces vides SlowTestCategory et FastTestCategory.

Maintenant, reprenons nos deux classes de tests en apportant quelques modifications :

  • Dans ProductDaoTest et CustomerDaoTest
import org.junit.experimental.categories.Category;

@Category(SlowTestCategory.class)
public class ProductDaoTest {
    @Test
    public void should_insert_new_row() {

    @Test
    @Category(FastTestCategory.class)
    public void should_build_businessKey() {

Concrètement, on a ajouté l’annotation @Category sur chaque méthode de test en précisant la catégorie souhaitée grâce aux interfaces que nous avons créées précédemment.

Dans CustomerDaoTest

import org.junit.experimental.categories.Category;

@Category(SlowTestCategory.class)
public class CustomerDaoTest {
    @Test
    public void should_insert_new_row() {

    @Test
    @Category(FastTestCategory.class)
    public void should_build_businessKey() {

Utilisation des catégories lors du build avec Maven

Maintenant que nos catégories sont définies, il nous faut indiquer à Maven quels tests nous souhaitons lancer et quand. Pour rappel, notre besoin est :

  • les tests tagués comme “Fast” et uniquement ceux-là doivent être lancés quand le développeur effectue un “mvn test” sur sa machine
  • sur tous les autres environnements, la même commande devra exécuter tous les tests sans distinction

Tout ce qui suit part du principe qu’on utilise Maven. Sur cette base, pour répondre à ce besoin, nous allons :

  • d’abord définir le comportement du plugin Surefire (celui chargé de l’exécution des tests unitaires) pour que, par défaut, il exécute tous les tests.
  • ensuite, surcharger ce comportement dans un profil “busyDev” afin que, dans ce cas seul, les tests rapides soient lancés.

Voici ce à quoi pourrait ressembler la définition par défaut du plugin Surefire

<plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-surefire-plugin</artifactId>
     <version>${surefire-version}</version>
</plugin>

Et la définition du profil busyDev

<profiles>
    <profile>
        <id>busyDev</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>${surefire-version}</version>
                    <configuration>
                         <groups>
                            com.ptngaye.junittutoriel.categories.FastTestCategory,
                         </groups>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Exécution des tests rapides en tant que “busyDev”

L’exécution de la commande suivante nous permet de lancer uniquement les tests de la catégorie “fast” :

mvn clean test -PbusyDev -q

junit-categories-busyDev

Exécution des tests avec un profile autre que dev

L’exécution de la commande

mvn clean test  -q

donne

junit-categories-default

Comme, on peut le voir, tous les tests du projets sont lancés sans distinction. J’ai rajouté exprès un test RttCalculator qui n’est rattaché à aucune catégorie et il est quand même pris en compte.

Quelques remarques sur les catégories

On peut faire quelques remarques sur les catégories :

  • on peut fonctionner de manière “classique” en exécutant tous les tests de toutes les classes de la suite et dans ce cas, on ne définit ni l’exclusion ni l’inclusion.
  • une méthode hérite de toutes les catégories de la classe. C’est pour cela que nous n’avons pas eu à mettre de catégorie SlowTestCategory sur les méthodes “should_insert_new_row”
  • les catégories ne se surchargent pas mais s’additionnent. C’est ce qui explique que nous pouvons rajouter une catégorie FastTestCategory au niveau des méthodes “should_build_businessKey” qui possèdent donc deux catégories : SlowTestCategoryet et FastTestCategory. Ceci implique qu’il faut faire attention pour ne pas skiper par erreur des tests à cause d’une mauvaise configuration. Par exemple, imaginons que celui qui définit le profil busyDev est plus tatillon (ou rigoureux). Ainsi, au lieu de se contenter de d’inclure les tests de la catégorie “FastTestCategory”, il prend soin d’exclure les tests de la catégorie SlowTestCategory.
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>${surefire-version}</version>
    <configuration>
         <groups>
             com.ptngaye.junittutoriel.categories.FastTestCategory,
         </groups>
         <excludedGroups>
             com.ptngaye.junittutoriel.categories.SlowTestCategory
         </excludedGroups>
     </configuration>
</plugin>

Dans ce cas, les méthodes tests “should_build_businessKey” seront skipées car taguées SlowTestCategory même si elles sont également taguées FastTestCategory

Exécution de tous les tests d’une catégorie depuis l’IDE.

Jusqu’ici, nous avons cherché à lancer nos tests depuis la ligne de commande avec Maven. Dans la plupart de nos IDE, il est également possible de lancer directement la commande mvn test en un clic. Cependant, si comme moi vous avez l’habitude d’un “CTRL + R” (MoreUnit sous Eclipse), d’un CTRL + MAJ +F10 (IntelliJ) ou encore d’un clic droit sur le test pour le lancer, alors on peut être déçu. En effet, il n’y a pas de solution “out of the box” pour lancer en une fois tous les tests d’une catégorie.

Les suites

Encore une fois, la première solution est l’utilisation des suites. En effet, comme on peut le voir en ouvrant la source de la classe Categories, on remarque qu’elle hérite de Suite (et est donc également un runner). Ainsi les catégories ne sont rien d’autre que des suites. Précédemment, j’avais indiqué que la catégorie était une interface ou une classe. Nous allons donc modifier nos deux catégories afin d’en faire des suites :

La catégorie “FastTestCategory”

import org.junit.experimental.categories.Categories;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Categories.class)
@Categories.IncludeCategory(FastTestCategory.class)
@Suite.SuiteClasses({
        ProductDaoTest.class,CustomerDaoTest
})
public class FastTestSuite {
}

La catégorie “SlowTestCategory”

import org.junit.experimental.categories.Categories;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Categories.class)
@Categories.IncludeCategory(SlowTestCategory.class)
@Categories.ExcludeCategory(FastTestCategory.class)
@Suite.SuiteClasses({
        ProductDaoTest.class,CustomerDaoTest
})
public class SlowTestCategory {
}

La solution ClasspathSuite

Le problème avec la solution précédente c’est que nous devons déclarer de manière exhaustive toutes les classes de tests dans la catégorie. Outre le fait que c’est pénible et difficilement maintenable, elle nous amène à déclarer deux fois les catégories :

  • d’abord dans la suite en précisant toutes les classes de tests concernées
  • ensuite dans la classe de test elle-même

La solution vient avec la bibliothèque ClasspathSuite. Cette bibliothèque existait avant l’introduction des catégories dans JUnit. Elle a été créée pour corriger le défaut originel des suites : l’obligation de devoir déclarer toutes les classes de tests dans la suite. ClasspathSuite permet au développeur de définir des patterns sur les classes de tests à inclure plutôt que d’avoir à les lister.

Concrètement, ClasspathSuite, qui donne son nom à la bibliothèque, est une sous-classe de Suite avec des fonctions de filtre sur les classes. Par défaut, une suite ClasspathSuite incorpore toutes les classes de tests du projet. C’est cette aptitude que nous allons exploiter en créant une suite AllTestSuite comme suit :

import org.junit.extensions.cpsuite.ClasspathSuite;
import org.junit.runner.RunWith;
@RunWith(ClasspathSuite.class)
public class AllTestSuite {
}

Ensuite, nous allons reprendre nos deux catégories et modifier l’annotation @Suite.SuiteClasses comme suit :

@Suite.SuiteClasses({
        AllTestSuite.class,
})

@Suite.SuiteClasses({
        AllTestSuite.class,
})

Ainsi, ClasspathSuite va nous permettre de récupérer automatiquement tous les tests du projet et de les injecter dans chacune de nos catégories.

Critique de la solution JUnit

Les catégories JUnit, implémentées à l’aide d’annotations, sont relativement simples à mettre en place même sur du code existant. Elles permettent de créer des blocs de tests avec différents niveaux de granularité. Au final, mes seules critiques concernent des éléments périphériques qui n’ont rien à voir avec le framework JUnit lui-même : la prise en charge des catégories dans Surefire et dans les IDE. Même le fait de devoir créer une interface par catégorie que beaucoup présentent comme un défaut semble à mon sens un avantage. En effet, en cas de refactoring, avoir une interface est plutôt un atout.

Gestion des catégories dans Surefire

On peut regretter de ne pas pouvoir définir une liste de catégories à l’aide d’une regex. Par ailleurs, il faut obligatoirement redéfinir le bloc plugin dans le POM. On ne peut pas, par exemple, préciser les tests à ignorer via la propriété “surefire.excludedGroups” directement en ligne de commande ou via la balise. Ce n’est pas rédhibitoire, mais cela rend le POM beaucoup plus verbeux qu’il ne devrait l’être.

Gestion des catégories dans les IDE

Grâce à la bibliothèque ClasspathSuite, on a une solution correcte. Cependant, on peut regretter de ne pas avoir une solution native pour lancer en une fois tous les tests d’une catégorie. De plus, on peut imaginer que le fait que cette feature soit toujours dans un package “org.junit.experimental”, au niveau de JUnit, n’encourage pas trop les éditeurs tiers à s’investir.

To be continued…

Dans cette première partie nous avons précisé les bases des tests avec JUnit et nous avons vu la solution apportée par JUnit à un besoin récurrent : le regroupement des tests. Dans le prochain post, nous verrons comment injecter des jeux de données dans un test et comment réaliser des intercepteurs de test.