Keyboard shortcuts

Touchez ← ou → pour naviguer les chapitres

Touchez S ou / pour chercher dans le livre

Touchez ? pour afficher ce message

Touchez Esc pour masquer ce message

Accueil > Programmer avec Java >

đŸ› ïž Tests unitaires

Survol et attentes

Dans un programme dĂ©composĂ©, c’est important de tester chaque mĂ©thode avant de l’intĂ©grer dans une mĂ©thode appelante. Ainsi, si une erreur se produit, on limite la recherche de l’erreur Ă  une seule mĂ©thode.

Définitions
Test unitaire
vĂ©rifier si une mĂ©thode produit le rĂ©sultat attendu pour un ensemble d’entrĂ©es spĂ©cifiques. Un test unitaire Ă©value une seule mĂ©thode, d’oĂč le terme “unitaire”.
Cas de test
un scĂ©nario (une entrĂ©e spĂ©cifique) qui doit ĂȘtre testĂ© pour une mĂ©thode. Chaque cas de test fait le lien entre les entrĂ©es et les sorties attendus. Un test unitaire inclut gĂ©nĂ©ralement tous les cas de test identifiĂ©es pour une mĂ©thode.
JUnit
un framework de test unitaire pour Java. Nous utiliserons la suite d’outils Java dans VS Code pour installer et exĂ©cuter les tests JUnit. De plus, ces outils crĂ©ent automatiquement les fichiers de test et les signatures des mĂ©thodes pour chaque test unitaire.
Framework
un ensemble d’outils et de conventions qui facilitent le dĂ©veloppement de logiciels. Un framework est plus vaste qu’un package ou une module de code, et la plupart des applications logicielles sont bĂątis Ă  l’aide de frameworks. JUnit est un framework pour les tests unitaires.
Test d’entrĂ©e/sortie
test unitaire qui doit tenir compte des valeurs normalement saisies via l’entrĂ©e standard et affichĂ©es Ă  la console. Ces tests sont plus difficiles Ă  automatiser parce qu’il faut temporairement rediriger l’entrĂ©e et/ou la sortie standard vers des sources de texte Ă©crites Ă  l’avance dans le test.

Objectifs d’apprentissage

À la fin de cette leçon vous devrez ĂȘtre en mesure de :

  • dĂ©crire c’est quoi un test unitaire et pourquoi on les utilise.
  • savoir comment choisir des cas de test pour une mĂ©thode donnĂ©e.
  • savoir comment utiliser les outils dans VS Code pour crĂ©er et exĂ©cuter des tests unitaires pour des programmes Java.

CritĂšres de succĂšs

ÉvaluĂ©s sommativement dans ce cours :

  • Je suis capable de rĂ©diger une liste de cas de test pour une mĂ©thode donnĂ©e et d’intĂ©grer ces cas dans le test unitaire.
  • Je suis capable d’adapter le gabarit de test d’égalitĂ© pour mes mĂ©thodes.

Aspirationnels - on n’a pas assez de temps dans ce cours pour devenir vraiment bons Ă  ces compĂ©tences, mais on peut les explorer :

  • Je suis capable de crĂ©er un fichier de test et les mĂ©thodes de test pour mes programmes en utilisant les outils de JUnit dans VS Code.
  • Je suis capable d’adapter le gabarit de test d’entrĂ©e et de test de sortie pour mes mĂ©thodes.

Pourquoi faire des tests unitaires?

Un programme devient trÚs rapidement large et complexe. On utilise déjà la décomposition pour gérer la complexité et la taille du programme : elle nous permet de résoudre une série de petits problÚmes.

Par contre, l’avantage de la dĂ©composition est largement gaspillĂ© si on ne vĂ©rifie pas le bon fonctionnement de chaque mĂ©thode - chaque morceau du problĂšme dĂ©composĂ© - avant de l’intĂ©grer dans la mĂ©thode plus haut dans la chaĂźne d’appels. La raison est la suivante : s’il y a une erreur dans une mĂ©thode qu’on n’a pas testĂ© mais on l’intĂšgre sans connaissance dans une autre mĂ©thode, lorsque on exĂ©cute Ă©ventuellement le programme, on ne sait pas si l’erreur vient de la mĂ©thode appelante ou de la mĂ©thode appelĂ©e. Ce problĂšme explose avec le nombre d’intĂ©grations de mĂ©thodes qu’on fait avant de tester.

Avec les tests unitaires, on peut isoler les erreurs Ă  une seule mĂ©thode, limitant la quantitĂ© de code Ă  vĂ©rifier pour corriger l’erreur. Le but est de tester chaque mĂ©thode et de s’assurer de son fonctionnement avant de l’intĂ©grer dans une autre mĂ©thode. Ainsi, si une erreur se produit, on sait qu’elle vient du plus rĂ©cent code qu’on a Ă©crit et non des autres mĂ©thodes qui existent dĂ©jĂ .

Un autre avantage des tests unitaires formelles, comme ceux avec le framework JUnit, est que si on modifie le programme, on peut relancer tous les tests que nous avons dĂ©jĂ  Ă©crits pour s’assurer que les modifications n’ont pas cassĂ© quelque chose qui fonctionnait dĂ©jĂ .

Cas de tests

En faisant un test unitaire, on doit savoir quel comportement est attendu de notre mĂ©thode, soit qu’est-ce qui constitue un rĂ©sultat acceptable. Les cas de tests sont simplement la description de ces comportements attendus pour une mĂ©thode donnĂ©e. C’est, en fait, la partie la plus importante de l’écriture des tests unitaires. Si on ne sait pas ce qu’on attend de notre mĂ©thode, on ne peut pas Ă©crire de tests pour vĂ©rifier si la mĂ©thode fonctionne correctement.

Déterminer quels sont les cas de test importants est un art. Il faut trouver un équilibre entre tester tous les cas possibles et tester seulement les cas les plus importants. Voici quelques pistes pour déterminer les cas de test pour une méthode :

  • Cas de test limites : les cas de test qui couvrent les valeurs aux limites de ce qui serait normalement attendu. Par exemple, si on doit donner une note entre 0 et 100, on devrait tester avec 0 et 100. De mĂȘme, si dans la zone des valeurs acceptĂ©s, il y a des limites pour diffĂ©rentes catĂ©gories, on devrait tester ces limites aussi. Par exemple, si la note de passage est de 50, on devrait tester avec 49 et 50.
  • Cas de test invalides : si notre programme doit gĂ©rer la qualitĂ© des entrĂ©es, il faut aussi prĂ©voir les cas de test qui couvrent les valeurs qui ne devraient pas ĂȘtre acceptĂ©es. Par exemple, si on doit donner une note entre 0 et 100, on devrait tester avec -1, 101, et des lettres.

Tableau de cas tests

Pour l’exemple prĂ©cĂ©dent, soit d’une mĂ©thode qui calcule la moyenne de deux notes et retourne le rĂ©sultat comme valeur de retour, on peut prĂ©parer le tableau de cas de test suivant :

IntentionEntréeSortie attendue
deux notes int valides80, 9085.0
deux notes double valides80.0, 90.085.0
notes limites0, 10050.0
notes limites0, 00.0
notes limites100, 100100.0
note négative-1, 90-1.0 // une valeur drapeau pour signaler un résultat invalide
note supérieur à 10080, 101-1.0 // une valeur drapeau pour signaler un résultat invalide

On peut reprĂ©senter ce mĂȘme tableau comme commentaire de bloc ou comme javadoc dans la mĂ©thode de test unitaire, comme ceci :

/*
 * Test la méthode `average` pour les cas suivants :
 * - deux notes int valides : 80, 90 => 85.0
 * - deux notes double valides : 80.0, 90.0 => 85.0
 * - notes limites : 0, 100 => 50.0
 * - notes limites : 0, 0 => 0.0
 * - notes limites : 100, 100 => 100.0
 * - note négative : -1, 90 => -1.0 // une valeur drapeau pour signaler un résultat invalide
 * - note supérieur à 100 : 80, 101 => -1.0 // une valeur drapeau pour signaler un résultat invalide
 */

Approche naĂŻve pour les tests unitaires

Sans utiliser des outils spĂ©cialisĂ©s, nous pouvons simplement lancer manuellement le programme aprĂšs avoir ajoutĂ© une nouvelle mĂ©thode. Naturellement, on aura probablement appelĂ© la mĂ©thode dans la logique globale du programme pour voir si elle fonctionne. Ça marche pour des programmes trĂšs simples mais c’est limitĂ© mĂȘme dans ces cas :

  1. S’il y a plusieur cas de test, on doit les exĂ©cuter un par un et comparer les rĂ©sultats manuellement, possiblement en insĂ©rant des System.out.println() pour afficher les rĂ©sultats qu’on aura Ă  retirer plus tard. Cela est une tĂąche lourde et sujette Ă  des erreurs.
  2. Si plusieurs parties du programme doivent s’exĂ©cuter avant d’arriver Ă  l’appel de la nouvelle mĂ©thode Ă  tester, on doit passer Ă  travers tout le programme pour chaque test. Cela est une perte de temps et d’énergie qui fait en sorte qu’on Ă©vite souvent de tester rigoureusement les mĂ©thodes.

Mieux mais sans les outils de test unitaire

Pour faire mieux que l’approche dĂ©crite ci-dessus, on peut tenter d’écrire des tests unitaires sans l’appui des outils, notamment parce que l’installation des outils peut ĂȘtre complexe et parce que ça nous Ă©vite d’apprendre comment utiliser les outils. En Ă©crivant nos propres tests, on peut dĂ©finitivement pallier aux deux problĂšmes soulevĂ©s ci-dessus :

  • on peut inclure les cas de test dans notre code de test
  • on peut inclure les comparaisons automatiques avec les rĂ©sultats attendus dans notre code de test
  • on peut exĂ©cuter les tests unitaires sans passer par la logique globale du programme

Par contre, tout ça exige la rĂ©daction de beaucoup de code additionnel (avec l’introduction presque garantie de nouvelles erreurs). Et tout ce nouveau code est probablement mieux Ă©crit et dĂ©finitivement dĂ©jĂ  validĂ© dans les outils de test unitaire.

De plus, le code de test “maison” est le plus facile Ă  Ă©crire et Ă  lancer dans la mĂȘme classe que les mĂ©thodes Ă  tester. Cela rend le code dans cette classe plus difficile Ă  lire, sans parler de la mĂ©thode main qui peut commencer Ă  ressembler Ă  quelque chose comme ceci :

Fichier: MyGreatProgram.java

void main() {
    // tests unitaires
    // testMethod1(); // masqué derriÚre un commentaire pour ne pas l'exécuter
    // testMethod2();
    testMethod3();


    // logique globale du programme
    // ...
}

void method1() {
    // ... code qu'on veut utiliser dans le programme
}

void testMethod1() {
    // ... code pour tester la méthode qu'on veut utiliser dans le programme
}

// ... reste des méthodes de la classe

Approche standard pour les tests unitaires avec JUnit 4

En se servant des outils existants, comme JUnit :

  • notre classe peut ĂȘtre Ă©crite exactement comme on l’a dĂ©crit avec la dĂ©composition du problĂšme sans code additionnel,
  • les tests peuvent ĂȘtre Ă©crits plus succinctement avec le code fourni par JUnit,
  • le lancement des tests est entiĂšrement indĂ©pendant du lancement de notre programme et
  • nous n’avons pas besoin de programmer comment afficher les rĂ©sultats des tests parce que JUnit le fait pour nous.

L’utilisation de JUnit nous impose pour la premiĂšre fois une structure de projet Java qui dĂ©passe un seul fichier :

  • le fichier pour notre code et
  • un autre fichier pour les tests unitaires.

La section suivante dĂ©crit la modification qu’il faut apporter Ă  nos programmes pour les tester avec JUnit.

Structure de projet pour les tests unitaires

On a vu dans une leçon sur les bases de Java que ce langage est utilisĂ© pour d’énormes projets logiciels et que le code peut ĂȘtre divisĂ© en plusieurs types d’unitĂ©s autonomes du plus grand (les logiciels et les frameworks) aux unitĂ©s intermĂ©diaires (les modules et les packages) et finalement aux unitĂ©s atomiques (les classes).

Parce que tous nos programmes jusqu’à prĂ©sent tenaient dans une seule classe, le compilateur Java nous permettait d’omettre une dĂ©claration de classe et de simplement Ă©crire des dĂ©clarations pour nos mĂ©thodes et nos variables globales.1 De plus, on n’avait pas Ă  se prĂ©occuper des mots-clĂ©s qui sont utilisĂ©s pour gĂ©rer la visibilitĂ© des Ă©lĂ©ments de notre classe (public, private) ni de comment les mĂ©thodes sont appelĂ©es (static ou non). On ne travaillera pas avec ces concepts dans ce cours (ils sont couverts dans le cours de 12e annĂ©e), mais on doit quand mĂȘme ajouter une chose Ă  notre code pour le tester avec JUnit : une dĂ©claration de classe.

La dĂ©claration de classe donne un nom Ă  notre code et permet au code dans la classe test d’y rĂ©fĂ©rer pour utiliser nos mĂ©thodes.

Ainsi, si on a un fichier Calculator.java avec le contenu suivant :

int add(int a, int b) {
    return a + b;
}

void main() {
    System.out.println(add(1, 2));
}

et on veut tester la méthode add, on doit ajouter une déclaration de classe autour de notre code pour le tester avec JUnit :

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    void main() {
        System.out.println(add(1, 2));
    }
}
  1. Utiliser l’outil “Mettre le document en forme” dans VS Code pour rĂ©tablir une bonne indentation aprĂšs avoir ajoutĂ© la dĂ©claration de classe (n’oubliez pas son accolade fermante Ă  la fin du code).
  2. Le nom de la classe doit ĂȘtre le mĂȘme que le nom du fichier et on devrait respecter les conventions Java pour les noms de classe/fichier : commencer par une majuscule et utiliser la casse chameau pour les noms composĂ©s.

    Pour renommer le fichier, au besoin, utiliser l’outil “Renommer le fichier” dans VS Code parce qu’il renommera automatiquement le nom de la classe et toutes les rĂ©fĂ©rences Ă  cette classe Ă  travers le projet.

Maintenant, le code dans la classe de test pourra créer un objet de type Calculator pour tester la méthode add(), comme on ajoute des objets de type Scanner pour utiliser ses méthodes next*().

Travailler avec JUnit dans VS Code et l'Extension Pack for Java

On va utiliser pour la premiĂšre fois l’option “Source Actions” dans VS Code pour crĂ©er des tests unitaires. Cette section vous montre comment le faire Ă©tape par Ă©tape.

ÉTAPE 1 : CrĂ©ez votre fichier dans un projet Java et Ă©crire au moins une de ses mĂ©thodes. Pour cet exemple, on peut utiliser le fichier Calculator.java avec le contenu suivant :

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    void main() {
        System.out.println(add(1, 2));
    }
}
ÉTAPE 2

Ouvrir le fichier dans VS Code et attendre une minute afin de laisser les outils Java s’activer.

ÉTAPE 3

Faites un clic droit n’importe oĂč dans le fichier et choisissez “Source Action
”. Vous devriez voir une option pour Generate Tests. Cliquez dessus.

Source Action | Generate Tests

ÉTAPE 4

Si c’est la premiĂšre fois qu’on fait ces Ă©tapes dans un projet, VS Code vous donnera une erreur et un bouton Enable tests. Cliquez dessus et choisir le framework JUnit. VS Code installera automatiquement les dĂ©pendances nĂ©cessaires pour JUnit dans votre projet, dans le sous-dossier lib.

Enable tests | Choisir JUnit | Installation de JUnit

Note : vous aurez à faire cette étape une seule fois par projet, mais vous aurez à la refaire si vous créez un nouveau projet.

ÉTAPE 5

Tapez Enter pour acceptez le nom proposé pour la classe de test, généralement [nom de ma classe]Test. Dans notre exemple, ce serait CalculatorTest.

ÉTAPE 6

SĂ©lectionnez la mĂ©thode que vous voulez tester. Dans notre exemple, on veut tester add. Cliquez sur Enter pour accepter. La classe sera créée avec la signature de la mĂ©thode de test pour add. Cochez seulement la plus rĂ©cente mĂ©thode, celle qui n’a pas encore de test unitaire.

choisir les tests à générer | classe teste générée

Notez qu’on ne doit pas tester main parce qu’il contient la logique globale du programme. main n’est pas une “unitĂ©â€ mais plutĂŽt “l’intĂ©gration” ultime de toutes les unitĂ©s de notre programme. Ça ne fait pas de sens de prĂ©parer des tests unitaires pour cette mĂ©thode.

ÉTAPE 7

Écrivez les tests unitaires pour les mĂ©thodes choisies. C’est Ă  cet Ă©tape qu’on doit considĂ©rer les cas de test et les implĂ©menter.

Les sections suivantes donnent des gabarits de tests unitaires que vous pouvez utiliser pour écrire vos tests.

ÉTAPE 8

Exécutez les tests unitaires en cliquant sur le bouton Run Test à cÎté de la méthode de test ou en cliquant sur le bouton Run All Tests en haut de la classe de test.

Exécuter les tests

Notez que s’il y a une seule classe dans votre projet qui contient des erreurs de syntaxe, vous recevrez un message d’erreur avant le lancement des tests parce que les outils Java compilent tout votre projet. Si l’erreur n’est pas dans la classe à tester ni dans la classe avec les tests, vous pouvez simplement cliquer sur le bouton Continue pour ignorer ces erreurs et lancer les tests.

ignore errors

ÉTAPE 9

Analysez les rĂ©sultats des tests dans la fenĂȘtre de sortie de JUnit. Si les tests sont tous rĂ©ussis, rien ne s’affichera et vous devrez vous rendre Ă  l’onglet “Test Results” pour voir la sortie. Si un test Ă©choue, vous verrez un message d’erreur dans la fenĂȘtre de sortie.

Résultats des tests

Notez que la sortie texte dans la partie gauche ne dit pas grand-chose d’utile. C’est plutĂŽt la partie droite qui donne l’état de chacun des tests. Le crochet vert est pour un test rĂ©ussi, le point rouge est pour un test Ă©chouĂ©. Vous pouvez cliquer sur le rĂ©sultat de chaque test pour plus de dĂ©tails ou pour les lancer de nouveau.

ÉTAPE 10 : Corrigez les erreurs dans votre code et rĂ©exĂ©cutez les tests jusqu’à ce qu’ils passent tous. Les erreurs peuvent se trouver dans le test unitaire ou dans la mĂ©thode que vous testez
 mais pas ailleurs! C’est l’avantage de faire des tests unitaires pour chaque mĂ©thode qu’on finit d’écrire.

ÉTAPE 11 : RĂ©pĂ©tez les Ă©tapes 3 Ă  10 pour chaque nouvelle mĂ©thode que vous Ă©crivez dans la classe principale du projet.

Gabarits - code de démarrage pour vos tests unitaires avec JUnit

Votre responsabilité principale en lien avec les tests unitaires est la définition des cas de tests pour chaque méthode que vous écrivez.

Par contre, vous pouvez aussi apprendre comment implémenter les tests unitaires qui appliquent ces cas de test à vos méthodes. Pour vous aider, les exemples de tests unitaires ci-dessous vous donnent du bon code de démarrage pour quatre cas communs :

  • un test d’égalitĂ©,
  • un test d’entrĂ©e (avec un Scanner global),
  • un test d’entrĂ©e (avec un Scanner local) et
  • un test de sortie.

Selon la structure de vos mĂ©thodes, vous aurez probablement Ă  combiner le code de tests diffĂ©rents, p. ex. d’égalitĂ© et d’entrĂ©e ou d’éntrĂ©e et de sortie pour avoir la bonne structure de test pour vos mĂ©thodes.

Structure de la classe de test

Voici un gabarit de base pour une classe de test unitaire oĂč vous remplacerez “MyClassname” par le nom de la classe que vous testez. Vous pouvez ajouter autant de mĂ©thodes de test que nĂ©cessaire dans cette classe.

import static org.junit.Assert.*; // pour les méthodes de comparaison
import org.junit.Test; // pour l'annotation @Test et les outils associés

public class MyClassnameTest {

    // déclaration d'un objet de la classe à tester
    // p. ex. Calculator calc = new Calculator();

    @Test
    // déclaration d'une' méthode de test pour une méthode dans votre code

    @Test
    // déclaration d'une méthode de test pour une autre méthode dans votre code

}

Test d’égalitĂ©

Les tests d’égalitĂ© sont pour des mĂ©thodes qui retournent une valeur. On compare la valeur retournĂ©e par la mĂ©thode avec la valeur attendue.

Exemple de base

Considérant notre méthode add dans la classe Calculator :

class Calculator {
    // ... reste du code de la classe

    int add(int a, int b) {
        return a + b;
    }
}

La classe de test produit par les outils de JUnit dans VS Code serait le suivant, avec quelques lignes additionnelles :

import static org.junit.Assert.*; // ajoutez cette ligne pour les méthodes de comparaison

import org.junit.Test;

public class CalculatorTest {

    Calculator calc = new Calculator(); // ajoutez cette ligne pour faire référence à votre code

    @Test
    public void testAdd() {

    }
}

On peut Ă©crire le test unitaire testAdd dans la classe CalculatorTestcomme suit Ă  l’intĂ©rieur de la classe de test :

    @Test
    public void testAdd() {
        /*
         * Cas de tests pour int add(int, int)
         *
         * Descr.   Entrées  Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * base     1, 2    3
         */

        assertEquals(3, calc.add(1,2));
    }

Deux points qui peuvent faire planter ce code :

  • La mĂ©thode assertEquals() ne sera pas reconnue si vous n’avez pas ajoutĂ© l’imporation static d’Assert au dĂ©but du fichier de test. Voir le gabarit pour la classe de test ci-dessus.
  • La mĂ©thode add() ne sera pas reconnue si vous n’avez pas créé une variable calc de type Calculator dans la classe de test. Voir le gabarit pour la classe de test ci-dessus.

Notez que assertEquals est une mĂ©thode de JUnit qui compare le premier argument avec le deuxiĂšme argument et lĂšve une exception si les deux arguments ne sont pas Ă©gaux. C’est la mĂ©thode la plus courante pour tester des valeurs de retour.

Il y a plusieurs autres détails à noter dans ce code :

  1. L’annotation @Test et la signature de la mĂ©thode ont peut-ĂȘtre Ă©tĂ© créées automatiquement si vous avez suivi les Ă©tapes ci-dessus. L’annotation @Test devient un point de lancement du code de test pour JUnit.
  2. On Ă©crit les cas de test dans un commentaire de bloc au dĂ©but de la mĂ©thode de test. Dans l’exemple, on a simplement inclut un cas de test, mais vous devrez considĂ©rer l’ensemble des cas Ă  tester dans la mĂ©thode.
  3. On utilise la méthode assertEquals pour implémenter le cas de test, soit comparer le résultat attendu avec la valeur de retour de la méthode testée. Il devrait y avoir un appel à assertEquals pour chaque cas de test.
Exemple pour comparer des 'double'

L’exemple de base fonctionne pour tous les types sauf les valeurs à virgule flottante (comme les double).

Avec les double, dĂ» Ă  la conversion inexacte entre le binaire (dans la machine) et le dĂ©cimal (dans la reprĂ©sentation du nombre), il y a toujours - ou presque - des erreurs d’arrondissement. Ainsi on ne peut jamais Ă©valuer l’égalitĂ© entre deux double directement comme on le fait avec les autres types de donnĂ©es.

L’algorithme Ă  utiliser se rĂ©sume Ă  :

  • valeur 1 = valeur 2 ± une marge d'erreur acceptable ou
  • valeur absolue de (valeur 1 - valeur 2) <= une marge d'erreur acceptable (la valeur absolue ignore le signe du rĂ©sultat)

Par exemple, pour des notes de cours à une décimale prÚs on pourrait note1 = note2 ± 0.1 ou note1 = note2 ± 0.05, selon votre préférence.

Pour les double, JUnit fournit une mĂ©thode assertEquals qui prend un troisiĂšme argument, la marge d’erreur acceptable. Par exemple, pour une mĂ©thode average qui retourne la moyenne de deux nombres Ă  une dĂ©cimale prĂšs, on pourrait Ă©crire le test unitaire comme suit :

    @Test
    public void testAverage() {
        /*
         * Cas de tests pour double average(double, double) :
         *
         * Descr.   Entrées     Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * base     80.0, 90.0  85.0
         */

        assertEquals(85.0, calc.average(80.0, 90.0), 0.1);
    }

Notez que nous fournissons la valeur attendue et la valeur de retour de la mĂ©thode testĂ©e comme avec l’exemple. Cependant, on ajoute aussi un troisiĂšme argument, la marge d’erreur acceptable. Dans cet exemple, on accepte une diffĂ©rence de 0.1 entre la valeur attendue et la valeur de retour.

Le choix de la marge acceptable dĂ©pend de la prĂ©cision requise par votre programme. Comme rĂšgle de base, on devrait choisir une marge d’erreur qui est plus petite que la prĂ©cision finale que nous voulons parce que les erreurs d’arrondissment s’accumulent Ă  chaque opĂ©ration.

Donc la marge de 0.1 si on veut des rĂ©sultats Ă  une place dĂ©cimale est trop gĂ©nĂ©reuse et on perd beaucoup de prĂ©cision. RĂšgle de base : utiliser une marge d’erreur au moins 3 chiffres de plus que la prĂ©cision finale dĂ©sirĂ©e. P. ex., si on veut une prĂ©cision finale de 0.1 on devrait choisir une marge d’erreur d’au maximum 0.0001. le test ci-dessus serait modifiĂ© Ă  :

assertEquals(85.0, calc.average(80.0, 90.0), 0.0001);

Test d’entrĂ©e

Pour tester une mĂ©thode qui sollicite des entrĂ©es de l’utilisateur via la console, on a deux options selon la façon dont le Scanner de la console est gĂ©rĂ© dans la classe du programme :

Scanner static et globalScanner passé comme paramÚtre (Scanner local)
algorithme pour Scanner globalalgorithme pour Scanner local

Dans le cas d’un Scanner global, parce que la vie de la variable est plus longue que la vie de la mĂ©thode, on doit s’assurer de rĂ©tablir sa valeur originale avant de quitter le test. Pour le faire, on doit s’assurer de copier sa valeur originale au dĂ©but du test.

Pour le Scanner local, ces Ă©tapes sont Ă©liminĂ©es, mais on doit s’assurer de passer le Scanner comme argument Ă  la mĂ©thode Ă  tester.

Exemple avec un Scanner global

Si on a la méthode getNameUsingGlobalScanner dans la classe Calculator :

import java.util.Scanner;

class Calculator {

    /** Scanner global pour toutes les méthodes de la classe */
    Scanner console = new Scanner(System.in);

    // ... reste du code de la classe

    String getNameUsingGlobalScanner() {
        System.out.print("Entrez votre prénom > ");
        String name = console.next(); // utilise le Scanner global
        console.nextLine(); // jeter tout aprĂšs le premier mot
        return name;
    }
}

On peut écrire le test unitaire testGetNameUsingGlobalScanner dans la classe CalculatorTest comme suit :

    @Test
    public void testGetNameUsingGlobalScanner() {
        /*
         * Cas de test pour String getName() qui utilise un
         * Scanner déclaré globalement
         * 
         * Descr.           Entrée          Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * normal           "david"         "david"
         * un caractĂšre     "A"             "A"
         * plusieurs mots   "David Crowley" "David"
         */

        /* PRÉPARATION */

        // garder une référence au Scanner original de calc
        Scanner original = calc.console;

        // définir les cas de test
        String testInput = "david\n" +
                "A\n" +
                "David Crowley\n"; // les \n sont les `Entrée` de l'utilisateur

        // créer une source d'entrées qui contient nos cas de tests
        InputStream testStream = new ByteArrayInputStream(testInput.getBytes());

        // passer la nouvelle source d'entrées au Scanner de calc
        calc.console = new Scanner(testStream); 

        /* TESTS */
        
        assertEquals("david", calc.getNameUsingGlobalScanner());
        assertEquals("A", calc.getNameUsingGlobalScanner());
        assertEquals("David", calc.getNameUsingGlobalScanner());

        /* NETTOYAGE */

        // rediriger le Scanner global de calc Ă  lire sa source originale
        calc.console = original;
    }

Note : avec l’ajout des InputStream et du Scanner, on doit ajouter import java.io.*; et import java.util.*; au dĂ©but du fichier de test, p. ex. :

import static org.junit.Assert.*;

import java.io.*;   // ici
import java.util.*; // et ici

import org.junit.Test;

public class CalculatorTest {
    // ... reste du code de la classe

}

Si on oublie, parfois les outils d’autocomplĂ©tion dans VS Code peuvent ajouter ces dĂ©clarations automatiquement, mais c’est quelque chose Ă  vĂ©rifier si vous avez des messages d’erreurs sur ces variables.

Exemple avec un Scanner local (passé en argument)

Une autre façon d’utiliser un Scanner dans un programme est de le dĂ©clarer localement, par exemple avec la mĂ©thode getName dans la classe Calculator :

import java.util.Scanner;

class Calculator {
    String getName(Scanner in) {
        System.out.print("Entrez votre prénom > ");
        String name = in.next(); // fait référence au Scanner passé en paramÚtre
        in.nextLine(); // jeter tout aprĂšs le premier mot
        return name;
    }

    void main() {
        Scanner console = new Scanner(System.in); // déclarer un Scanner local
        //... autre code
        String name = getName(console); // passer le Scanner local comme argument
        //... autre code
    }
}

Le test unitaire testGetName dans CalculatorTest serait alors :

    @Test
    public void testGetName() {
        /*
         * Cas de test pour String getName(Scanner)
         * 
         * Descr.           Entrée          Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * normal           "david"         "david"
         * un caractĂšre     "A"             "A"
         * plusieurs mots   "David Crowley" "David"
         */

        /* PRÉPARATION */

        // définir les cas de test
        String testInput = "david\n" +
                "A\n" +
                "David Crowley\n"; // les \n sont les `Entrée` de l'utilisateur

        // créer un Scanner qui lit nos entrées de test au lieu de l'entrée standard
        InputStream testStream = new ByteArrayInputStream(testInput.getBytes());
        Scanner testScanner = new Scanner(testStream);

        /* TESTS */

        assertEquals("david", calc.getName(testScanner));
        assertEquals("A", calc.getName(testScanner));
        assertEquals("David", calc.getName(testScanner));

        /* NETTOYAGE */

        // Le nouveau Scanner est détruit automatiquement à la fin de cette méthode
    }

Note : avec l’ajout des InputStream et du Scanner, on doit ajouter import java.io.*; et import java.util.*; au dĂ©but du fichier de test, p. ex. :

import static org.junit.Assert.*;

import java.io.*;   // ici
import java.util.*; // et ici

import org.junit.Test;

public class CalculatorTest {
    // ... reste du code de la classe

}

Si on oublie, parfois les outils d’autocomplĂ©tion dans VS Code peuvent ajouter ces dĂ©clarations automatiquement, mais c’est quelque chose Ă  vĂ©rifier si vous avez des messages d’erreurs sur ces variables.

Test de sortie

Les tests de sortie sont utiles pour les méthodes void qui passent leurs résultats à la console. On peut utiliser la redirection de la sortie standard pour capturer les messages affichés par la méthode et les comparer avec les messages attendus.

Exemple de redirection de System.out pour les tests

Considérant la méthode sayName dans la classe Calculator :

    void sayName(String name) {
        System.out.println("Bonjour " + name);
    }

On peut écrire le test unitaire testSayName dans la classe CalculatorTest comme suit :

    @Test
    public void testSayName() {
        /*
         * Cas de test pour void sayName(String)
         * 
         * Descr.           Entrée          Sortie attendue
         * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         * normal           "David"         "Bonjour David"
         * un caractĂšre     "A"             "Bonjour A"
         * plusieurs mots   "David Crowley" "Bonjour David Crowley"
         * 
         */

        /* PRÉPARATION */

        // garder une référence à la sortie standard originale
        PrintStream original = System.out;

        // rediriger la sortie standard vers un flux de sortie temporaire
        OutputStream outContent = new ByteArrayOutputStream();
        System.setOut(new PrintStream(outContent));
    
        /* TESTS */

        calc.sayName("David");
        calc.sayName("A");
        calc.sayName("David Crowley");

        String expectedOutput = "Bonjour David\nBonjour A\nBonjour David Crowley\n";

        assertEquals(expectedOutput, outContent.toString().replace("\r\n", "\n")); // le replace() est nécessaire sur Windows (encodage de fin de ligne différent)

        /* NETTOYAGE */

        // rétablir la sortie standard originale
        System.setOut(original);
    }

Note : on applique la mĂ©thode de traitement de texte .replace("\r\n", "\n") sur la sortie pour s’assurer que les diffĂ©rents types de retour de ligne ne causent pas un Ă©chec de la comparaison. Windows utilise \r\n pour le retour de ligne, alors que Linux, MacOS et nos propres instructions Java utilisent seulement \n.

Note : avec l’ajout du PrintStream et des OutputStream, on doit ajouter import java.io.*; au dĂ©but du fichier de test, p. ex. :

import static org.junit.Assert.*;

import java.io.*;   // ici

import org.junit.Test;

public class CalculatorTest {
    // ... reste du code de la classe

}

Si on oublie, parfois les outils d’autocomplĂ©tion dans VS Code peuvent ajouter ces dĂ©clarations automatiquement, mais c’est quelque chose Ă  vĂ©rifier si vous avez des messages d’erreurs sur ces variables.

Les exemples complets

Vous pouvez voir le code complet pour les classes Calculator et CalculatorTest dans les fichiers Calculator.java et CalculatorTest.java.

Exercices

đŸ› ïž Pratique

Travaillez dans le répertoire GitHub partagé par votre enseignant pour la pratique et les exercices

  1. Créez un fichier texte nommé Cas-de-tests.txt dans votre réprtoire de travail.

    1. Écrivez le tableau de cas de test pour une mĂ©thode qui retourne true si un nombre est pair et false sinon. La signature de cette mĂ©thode est boolean isEven(int number). Quels sont les cas normaux? Est-ce qu’il y a des cas limites ou invalides pour cette mĂ©thode?
    2. Écrivez le tableau de cas de test pour une mĂ©thode qui retourne une note en lettres pour une note numĂ©rique. La signature de cette mĂ©thode est char letterGrade(double average). Les lettres sont F (moins de 50), D (50 Ă  59), C (60 Ă  69), B (70 Ă  79) et A (80 Ă  100). Quels sont les cas normaux? Quels sont les cas limites pour cette mĂ©thode, se rappelant que la moyenne est une valeur dĂ©cimale (il y en a beaucoup!)? Quels sont les cas invalides?
  2. Voici une implémentation de la logique de la méthode isEven décrit dans le premier exemple.

    class NumberUtils {
        boolean isEven(int number) {
            return number % 2 == 0;
        }
    }
    
    • CrĂ©ez une copie de ce fichier et nommez-le NumberUtils.java.
    • Utilisez les outils de VS Code pour crĂ©er la classe de test et la squelette de test unitaire pour cette mĂ©thode.
    • Ajoutez une dĂ©claration globale (dans la classe NumberUtilsTest) pour un objet NumberUtils : NumberUtils utils = new NumberUtils();
    • Vous devrez traduire votre tableau de cas de test (Ă©crit plus haut) en commentaire de bloc Ă  l’intĂ©rieur de votre mĂ©thode de test.
    • ImplĂ©menter des tests d’égalitĂ© pour cette mĂ©thode, un test par cas de test identifiĂ©. Servez vous du gabarit comme point de dĂ©part.
    • ExĂ©cutez les tests.
    • Prenez une capture d’écran de l’onglet “Test Results” pour montrer que vos tests ont passĂ©. L’enregistrez comme 4-unit-test.png dans le dossier “captures”.

  1. Depuis JEP 463 (pleinement intĂ©grĂ© aux outils Java22+) ↩

© 2022-2025 David Crowley