Les tests
Introduction
Quand un client commande un développement, un cahier des charges est rédigé décrivant des fonctionnalités de haut niveau sous forme de cas concrets d’utilisation. C’est ce document qui servira plus tard à valider les développements effectués. Ces cas d’utilisation vont mettre en action de nombreux objets développés par l’équipe. Ces objets vont communiquer entre eux avec des méthodes et des chronologies mûrement réfléchies pendant l’analyse. Chaque échange s’effectuera la plupart du temps avec des paramètres aller et retour. Les rangs admissibles de ces paramètres seront connus et ces objets fonctionneront généralement parfaitement bien quand ils recevront ce qu’ils attendront au moment où ils les attendront. Mais que se passera-t-il quand le cadencement s’emballera ou que les paramètres passés seront hors limites ?
La solidité d’une application se révèle dans les cas extrêmes par des traitements adaptés des défauts et une bonne protection des données. Pour gagner ce degré de fiabilité, il faut avant tout que chaque maillon de la chaîne reste stable, quelles que soient ses conditions d’exploitation. Pour cela, il faut les éprouver en les poussant dans leurs limites....
Environnement d’exécution des tests unitaires
Il est toujours possible d’écrire des « petites applications » autonomes permettant d’éprouver les objets de la future « grande application ». Par exemple, un code chargé dans une console pourra instancier la classe à tester, puis dérouler une série d’appels affichant des messages d’erreur ou écrivant les résultats dans un fichier. C’est tout à fait possible, mais… pas très pratique.
IntelliJ IDEA avec l’environnement de tests Java JUnit 5 simplifient la rédaction, l’exécution et l’analyse des tests unitaires. Pas besoin de petites applications autonomes ; IntelliJ IDEA propose directement la préparation d’un ensemble de tests que le développeur pourra jouer en totalité, en groupe (playlist) ou individuellement grâce à l’explorateur de tests. Les résultats des tests sont synthétisés dans une vue type arbre utilisant les couleurs jaune et vert, et permettant d’aller rapidement, en cas d’échec, sur la ligne de code défaillant. Il est possible d’exécuter les tests en mode Debug et donc de « tracer » les méthodes appelées dans les objets à fiabiliser.
Ce chapitre ne traite que d’une...
Le projet avec tests unitaires
Les tests sont des méthodes de classes que l’on ajoute, la plupart du temps, au projet contenant les classes à tester, en s’aidant des assistants d’IntelliJ IDEA.
Faites un premier projet, toujours de type console, dénommé… demotests contenant le package demo.tests. Ce projet figure dans le répertoire Chap9\demo_tests du .zip accompagnant cet ouvrage.
Ajoutez au package demo.tests une classe Vecteur ayant le contenu suivant :
package demo.tests;
public class Vecteur {
private int x;
private int y;
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public void Ajouter(Vecteur v)
{
x += v.x;
y += v.x; //Erreur de codage
// aurait dû être y += v.y;
} ...
La classe de tests
Placez votre souris sur la classe Vecteur et cliquez sur More Actions.
Sélectionnez Create Test.
Validez l’insertion des tests dans la même racine.
Dans l’assistant de création de tests, sélectionnez la bibliothèque JUnit5, puis cliquez sur le bouton Fix pour démarrer son installation dans le projet.
Gardez le nom de la classe de tests VecteurTest et le nom du package de destination demo.tests.
Sélectionnez la méthode Ajouter comme méthode à tester.
Ouvrez la classe VecteurTest et utilisez l’assistant pour effectuer les ajouts nécessaires.
Le code de départ de notre classe de test est le suivant :
package demo.tests;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class VecteurTest {
@Test
void ajouter() {
}
}
On y trouve le package contenant la classe VecteurTest qui elle-même contient la méthode ajouter.
Chaque méthode de tests doit être précédée par l’attribut @Test qui la distinguera d’éventuelles autres méthodes de la même classe.
Il se peut que l’assistant ait défini totalement le chemin du package Test en écrivant...
Contenu d’une méthode de test
Le test va être exécuté directement dans l’environnement de développement et ne nécessitera donc ni méthode main d’une classe et ni projet particulier. Le code de la méthode de test doit instancier la classe cible, appeler une de ses méthodes puis valider le comportement attendu. Normalement, dans le cadre de tests unitaires, il ne doit y avoir qu’une action sur l’objet par test.
Par exemple, pour une méthode Additionner, le test vérifie que si 2 et 3 sont passés en paramètres, alors le résultat retourné sera bien 5.
Les vérifications utilisent la classe Assertions du package org.junit. jupiter.api qui propose une collection de méthodes offrant des services beaucoup plus complets que ceux associés au mot-clé assert utilisé jusqu’alors dans cet ouvrage.
Parmi ce jeu de méthodes se trouvent la méthode assertTrue et ses surcharges.
(extrait de https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Assertions.html)
Dans cette forme, cette méthode permet de vérifier qu’une condition est vraie et, si elle ne l’est pas, de retourner un message de votre choix dans l’explorateur de tests.
Certaines méthodes de la classe Assertions proposent une version avec message et une version...
Traitements de préparation et de nettoyage
L’exécution des tests peut être précédée par des traitements d’initialisation et suivie par des traitements de « nettoyage ». Ces traitements facultatifs sont écrits dans des méthodes décorées d’attributs spéciaux qui définissent à quel moment elles seront exécutées par le script.
Attribut de la méthode en JUnit5 |
Quand sera exécutée la méthode |
@BeforeAll |
Une fois au tout début de la série de tests de la classe. |
@BeforeEach |
Avant chaque test. |
@AfterEach |
Après chaque test. |
@AfterAll |
Une fois à la fin de l’exécution de la série de tests de la classe. |
Extrait de code montrant la syntaxe de toutes les méthodes d’initialisation
package demo.tests;
import org.junit.jupiter.api.*;
public class AutresTests {
public AutresTests(){
System.out.println("Constructeur de la classe de tests");
}
@BeforeAll
public static void avantTout(){
System.out.println("Avant de lancer les tests (...)");
}
@AfterAll
public static void apresTout(){ ...
Les tests avec paramètres externes
Imaginons que nous ayons à tester le comportement d’une méthode dans des centaines de cas d’utilisation. Écrire et maintenir la collection d’informations dans le code sera difficile et tout changement nécessitera la recompilation des tests : pas très pratique ! Externaliser ses listes dans des fichiers puis effectuer leurs chargements et leurs itérations dans le test lui-même est envisageable, mais est-ce le rôle du test ? S’appuyer sur l’environnement de développement pour alimenter nos tests en données est, de loin, la solution la plus confortable et c’est ce que nous propose JUnit 5 ! On parle alors de « data-driven unit tests »...
La démonstration qui suit utilise une collection au format CSV (Comma-Separated Values). C’est en l’occurrence un fichier de format texte très simple où chaque ligne représente une collection de paramètres séparés par des virgules. On peut facilement obtenir un fichier .csv à partir d’un fichier Excel grâce à sa fonction d’exportation.
Mettons en place notre premier « data-driven unit test » pour vérifier le fonctionnement d’une méthode retournant une chaîne de caractères. Par exemple, si on lui passe « Hello », elle doit retourner...
Les suites de tests
Nous avons vu comment construire des classes contenant des méthodes de tests. Ces tests peuvent être exécutés manuellement depuis IntelliJ IDEA. La manipulation peut devenir rapidement rébarbative s’il y a un nombre important de classes de tests. De plus, si nous décidions de créer une chaîne de build, c’est-à-dire une procédure permettant de compiler et de mettre en forme le produit final, alors nous souhaiterions automatiser l’exécution des tests. C’est pour cela que nous allons nous intéresser à ce qui s’appelle une suite de tests.
Une suite de tests est un objet qui va contenir une liste de tests à effectuer. L’objectif de cette fonctionnalité est d’automatiser des tests pour vérifier que les nouvelles fonctionnalités sont actives et qu’il n’y a pas de régression sur les anciennes. Vous allez pouvoir construire une « playlist » de tests parmi toutes les méthodes que vous avez codées.
Exercice
1. Énoncé
Vous devrez écrire :
-
Une classe ClasseChaine contenant une méthode RetourneInitiales permettant de retourner les initiales des nom et prénom passés en paramètre sous forme d’une chaîne comme ci-dessous :
String initiales = ClasseChaine.RetourneInitiales("Andreas Dulac");
// initiales doit contenir "A.D."
-
Si la méthode reçoit un mauvais paramètre, elle doit retourner une chaîne vide.
-
Une série de tests unitaires permettant de vérifier que tous les cas d’utilisation de la méthode ne provoquent pas de dysfonctionnement.
2. Corrigé
Ce projet figure dans le répertoire Chap9\lab_tests du .zip accompagnant cet ouvrage.
Les cas d’erreur sont les suivants :
-
Le paramètre étant de type String est, par nature, nullable. La valeur null passée en tant que paramètre ne doit pas provoquer de dysfonctionnement.
-
Le cas d’une chaîne vide doit être pris en compte.
-
Le cas d’une chaîne ne contenant qu’un seul mot doit également être testé.
package lab.tests;
import static org.junit.jupiter.api.Assertions.*;
public class ClasseChaineTest {
@org.junit.jupiter.api.Test
void testCasNormal() {
String initiales...