Publié par
Il y a 4 années · 8 minutes · Craft, Java / JEE

Les tests unitaires paramétriques avec JUnitParams, une alternative à Junit Parameterized

Lorsque l’on souhaite tester un même comportement sur différentes données, on peut soit :

  • Développer plusieurs méthodes de tests qui vérifient le même code avec des entrées différentes ;
  • Utiliser une méthode de test paramétrique.

Les tests paramétriques permettent d’exécuter une même méthode de test sur des données différentes (les paramètres). Cela permet de ne pas avoir à écrire de multiples tests unitaires pour tester des entrées différentes.

Junit permet de faire des tests unitaires paramétriques, quoique de manière peu pratique et concise. Il existe une bonne alternative qui efface ces problèmes : JUnitParams, dont la première version stable (1.0.0) est sortie en mai 2013, et la dernière version (1.0.2) en juin 2013. Dans cet article, nous allons comparer l’aspect test paramétrique de ces deux frameworks à travers un exemple simple.

Avec Junit

Le framework de test unitaire Junit permet de développer des tests paramétriques. Pour cela, il fournit un runner (la classe avec laquelle les tests unitaires sont lancés) : Parameterized.

Utiliser Parameterized

Pour cet exemple, nous allons tester une classe fournissant des méthodes utilitaires s’appliquant à des numéros de téléphone. Par exemple, testons une méthode isValidPhoneNumber(String number) qui vérifie si la chaine de caractères passée en paramètre est un numéro de téléphone bien formé. Avec Junit, les différentes étapes sont les suivantes :

  1. Pour utiliser le runner Parameterized, il faut annoter la classe de test de la manière suivante :

    @RunWith(Parameterized.class)
  2. Il faut ensuite créer une méthode static annotée @Parameters fournissant les paramètres du test (entrées, résultat attendu) sous forme de Collection d’objet, où chaque élément de la collection est un objet (généralement un tableau) contenant les paramètres du test :
    @Parameters
    public static Collection<Object[]> params() {
        return Arrays.asList(
                new Object[] { "1", false},
                new Object[] { "0123456789", true}
            );
    }
  3. Le runner Parameterized Parameterized va injecter les valeurs fournies par la précédente méthode dans des attributs de classe afin de les rendre accessibles aux méthodes de test. Par conséquent, il faut ajouter les attributs et le constructeur nécessaires à cette injection :
    private final String phoneNumber;
    private final boolean isValidPhoneNumber;
    
    public PhoneUtilsJunitParameterizedTest(final String phoneNumber, final boolean isValidPhoneNumber) {
        this.phoneNumber = phoneNumber;
        this.isValidPhoneNumber = isValidPhoneNumber;
    }
  4. Et enfin, il faut créer la méthode de test. Ici nous testerons la méthode isValidPhoneNumber de la classe utilitaire PhoneUtils :
    @Test
    public void isValidPhoneNumberNumberTest() {
        final boolean result = PhoneUtils.isValidPhoneNumber(phoneNumber);
        assertThat(result).isEqualTo(isValidPhoneNumber);
    }
  5. On observe que les paramètres se manipulent via les attributs d’instance, où les données de la méthode annotée @Parameters ont été injectées.

Un test de plus…
Le runner Parameterized fonctionne de la manière suivante : pour chaque élément de la collection retournée par la méthode (qui doit être unique) annotée @Parameterized, chaque test de la classe est lancé. Imaginons à présent que l’on souhaite ajouter une méthode de test unitaire pour tester une autre méthode,  isMobilePhoneNumber, qui vérifie si un numéro de téléphone est valide. Il nous faut :

  • Ajouter les paramètres nécessaires à chaque élément de la collection :
    @Parameters
    public static Collection<Object[]> params() {
        return Arrays.asList(
                new Object[] { "1", false, false},
                new Object[] { "0123456789", true, false},
                new Object[] { "0606060606", true, true}
                );
    }

    où la première valeur est le paramètre d’entrée, la deuxième le résultat attendu dans la première méthode de test, et la troisième le résultat attendu dans notre nouveau test.

  • Comme pour le premier test, il faut également ajouter un attribut et donc un paramètre au constructeur :
    private final String phoneNumber;
    private final boolean isValidPhoneNumber;
    private final boolean isMobilePhoneNumber;
    
    public PhoneUtilsJunitParameterizedTest(final String phoneNumber, final boolean isValidPhoneNumber, final boolean isMobilePhoneNumber) {
        this.phoneNumber = phoneNumber;
        this.isValidPhoneNumber = isValidPhoneNumber;
        this.isMobilePhoneNumber = isMobilePhoneNumber;
    }
  • Et enfin, il faut créer la nouvelle méthode de test qui cette fois teste la méthode isMobilePhoneNumber :
    @Test
    public void isMobilePhoneNumberTest() {
        final boolean result = PhoneUtils.isMobilePhoneNumber(phoneNumber);
        assertThat(result).isEqualTo(isMobilePhoneNumber);
    }

Visualisation des résultats des tests dans Eclipse

Dans l’EDI Eclipse, le résultat des tests apparaît comme suit :

ecl1.png

On observe ce que l’on a noté plus haut sur le fonctionnement de Parameterized : pour chaque série de paramètres, chaque test de la classe est lancé.

Observations

Même s’il permet de développer des tests paramétriques, plusieurs problèmes apparaissent au fur et à mesure de l’écriture des méthodes de tests.

Le premier est la lourdeur de l’écriture des tests : un test unitaire requiert un attribut correspondant à chaque paramètre, un constructeur correspondant et une méthode fournissant les paramètres.

Conséquence de ce premier point, l’ajout d’une méthode de test unitaire oblige à modifier la classe à trois endroits en dehors du test lui-même : les paramètres évidemment, mais aussi les attributs et le constructeur.

Dans l’exemple que nous avons pris, l’ajout d’une deuxième méthode de test ne complique pas trop les tableaux de paramètres, mais dans le cas plus réaliste d’une classe testée comportant de nombreuses méthodes, on peut facilement se retrouver avec des lignes du type :

{param1test1, param2test1, expectedResultTest1, param1Test2, expectedResultTest2, param1test3, param2test3, param3test3, expectedResultTest3}

Ou bien pire ! Les éléments de la collection fournissant les paramètres se retrouvent rapidement illisibles en mélangeant les données utilisées lors des différents tests, ce qui affecte aussi la maintenabilité.

Autre conséquence du fonctionnement du runner Parameterized (rappel : chaque test de la classe est lancé une fois par élément de la collection retournée par la méthode fournissant les paramètres), si l’on ajoute des tests non paramétriques dans la classe de test, ils seront lancés autant de fois qu’il y a d’éléments dans la collections de paramètres, alors que leurs entrées sont invariantes !

Il est évidemment possible d’éclater les tests en plusieurs classes (c’est même le choix le plus propre), mais cela multiplie alors les classes de test pour une seule classe testée.

Il existe une solution répondant à ces problèmes et évitant de tels contournements : JUnitParams.

Avec JUnitParams

Tout comme Junit, JUnitParams (http://code.google.com/p/junitparams) fournit son propre runner de tests paramétriques : JunitParamsRunner.

Pour les projets utilisant Maven, il suffit d’ajouter la dépendance JUnitParams :

<dependency>
    <groupId>pl.pragmatists</groupId>
    <artifactId>JUnitParams</artifactId>
    <version>1.0.2</version>
    <scope>test</scope>
</dependency>

Utiliser JUnitParamsRunner

Pour écrire un test unitaire paramétrique avec JUnitParams, la démarche est la suivante :

  1. La première étape est de spécifier le runner en annotant la classe de test :
    @RunWith(JUnitParamsRunner.class)
  2. Créons ensuite notre méthode de test, en l’annotant avec @Parameters, fournie par JUnitParams :
    @Test
    @Parameters
    public void isMobilePhoneNumberTest(final String phoneNumber, final boolean expectedResult) {
        final boolean result = PhoneUtils.isMobilePhoneNumber(phoneNumber);
        assertThat(result).isEqualTo(expectedResult);
    }
  3. Il ne reste plus qu’à fournir les paramètres à notre méthode. JUnitParams propose plusieurs manières de faire : directement par annotation, par méthode, par classe externe, ou par lecture depuis une ressource externe (ex.: CSV). Nous allons utiliser une méthode pour fournir les paramètres au test.
  4. private Object[] parametersForIsMobilePhoneNumberTest() {
        return new Object[][] {
                {"1", false},
                {"0123456789", false},
                {"0606060606", true}
        };
    }

    Par convention, la méthode fournissant les paramètres est nommée de la façon suivante : parametersForNomDeLaMéthodeDeTest.

Et c’est tout !

Visualisation des résultats des tests dans Eclipse

Dans l’EDI Eclipse, le résultat des tests apparaît comme suit :

ecl2.png

On visualise ainsi la différence de fonctionnement avec JUnit classique : ici, les éléments de plus haut niveau ne sont pas les paramètres, mais les tests eux-mêmes.

Conclusion

Avec JUnitParams, nous avons vu comment écrire des tests unitaires paramétriques :

  • Légers à implémenter : il suffit d’une annotation sur la classe, sur la méthode de test et d’une méthode fournissant les paramètres ;
  • Lisibles : chaque méthode de test dispose de ses propres paramètres, donc ceux-ci restent très lisibles. De plus, le fait que les paramètres apparaissent dans la signature de la méthode de test facilite encore plus la compréhension du fonctionnement du test ;
  • Indépendants : les tests paramétriques ont chacun leur propre source de paramètres, ce qui permet également de mélanger tests paramétriques et non paramétriques dans la même classe.

En outre, JUnitParams est très flexible et propose d’autres manières de charger les paramètres : depuis une classe externe, directement au format texte dans l’annotation, ou encore par fichier CSV.

N’hésitez donc pas à alléger vos classes de tests unitaires avec JUnitParams !

Bastien Bonnet

Bastien est un développeur disposant de 4 ans d’expérience. Il est passionné par le développement de logiciel de qualité (code clair, facile à maintenir, robuste face aux régression (tests automatisés)). Agiliste convaincu, il s’inscrit parfaitement dans le mouvement du software craftsmanship. Il est convaincu et investi dans le partage de connaissance pour améliorer le niveau technique et les compétences de son équipe.

2 thoughts on “Les tests unitaires paramétriques avec JUnitParams, une alternative à Junit Parameterized”

  1. Publié par Clément HELIOU, Il y a 4 années

    Merci pour cet article qui pointe un manque criant du framework JUnit.
    Quelques remarques cependant:

    – Le domaine couvert par JUnit Params me semble se rapprocher beaucoup plus de la notion de théories (https://github.com/junit-team/junit/wiki/Theories) que de celle de tests paramètrés. Il est vrai que les noms choisis par JUnit sont loin d’être clairs. Les seconds sont en fait plus proches de la notion de fabriques, notamment aperçue dans le framework TestNG (http://testng.org/doc/documentation-main.html#factories). Dans les 2 approches de JUnit, l’utilisation est franchement lourde et ne permet pas, notamment, d’avoir plusieurs jeux de données dans une même classe de tests.

    – La solution apportée par JUnit Params me semble relativement complète et, sur ce point, place JUnit au même niveau que TestNG. Néanmoins, on peut regretter qu’elle ne soit pas inclue directement dans le framework et qu’il faille un développement externe pour en faire usage. Cette librairie étant très jeune et l’usage de TestNG apportant en plus d’autres avantages non négligeables (voir cette comparaison que j’ai écrit récemment: http://mister-tie.fr/blog/2013/08/07/testng-peut-il-detroner-junit-33/), je conseillerai plutôt de se pencher vers ce dernier.

  2. Publié par MALLEK Ahmed, Il y a 2 années

    Bonjour,
    Merci Bastien Bonnet pour cette API et pour l’explication dans l’article.
    J’ai commencé l’utilisation de cet API il y a quelques mois et c’est très efficace.
    Néanmoins je suis confronté à une limite d’exécution lorsque je veux utiliser junitparams avec les categories, lorsque je fais dans ma classe de test suite @RunWith(Categories) ça ne fonctionne plus.
    Y’a t’il une piste pour le faire, merci d’avance.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *