Dans la pratique courante, les tests unitaires permettent de détecter les failles dans la logique de programmation afin d’améliorer la qualité du code. Les tests unitaires sont utilisés pour s’assurer que le code fonctionnera comme prévu en cas de changements ultérieurs. L’objectif consiste à vérifier que les fonctionnalités développées dans notre application retourne les résultats attendus !
JUnit est un framework de tests unitaires pour Java. Les IDE comme IntelliJ IDEA, Eclipse, NetBeans et Visual Studio Code et les et les outils de compilation (Gradle, Maven et Ant) prennent en charge JUnit en mode Debug.
JUnit 5, la dernière version, fonctionne avec Java 8 ou plus.
JUnit 4 est encore utilisé.
De petites différences de syntaxe existent entre la version 4 et version 5.
Pour organiser le code, un projet possède généralement deux sources :
Pour chaque classe du projet à tester, on va créer une classe portant le même nom avec le suffixe Test.
Attention ! Il faut la même structure de package dans les 2 environnements.
RelevesNotes.java -> RelevesNotesTest.java
On va aussi créer pour chaque méthode de la classe à tester une (ou des) méthode(s) dans la classe de test.
Le but est de couvrir la majorité du code, c’est-à-dire que d’une part chaque méthode non triviale soit testée (les getters sont généralement des méthodes triviales) et d’autre part, qu’à l’intérieur d’une méthode testée, on couvre tous les cas pouvant se produire.
Dans l’exemple ci-dessous, il y aurait 5 cas à tester :
public static Cellule cellule(String cell) { x = ++x % xMax; if(cell.startsWith("C")) { int size = Integer.parseInt(cell.substring(1)); fourmiList = new ArrayList<Fourmi>(size); for (int i = 0; i<size; i++) { fourmiList.add(new Fourmi(x, y)); } return new Cellule(true, size); } else if (cell.equals("0")) { return new Cellule(); } else if (cell.equals("-1")) { return new Cellule(false); } else if (cell.matches("[0-9]*")) { return new Cellule(Integer.parseInt(cell)); } else { throw new UnsupportedOperationException("code cellule inconnu : "+cell); } }
Comme vous l’avez déjà remarqué à plusieurs reprises avec Java et SpringBoot, nous utilisons de nombreuses annotations.
Une annotation permet de marquer certains éléments du langage Java afin de leur ajouter une propriété particulière. Ces annotations peuvent ensuite être utilisées à la compilation ou à l’exécution (grâce à l’API de réflection java.lang.reflect) pour automatiser certaines tâches…
Nous pourrions créer nos propres annotations mais ce n’est pas le sujet de ce cours dans lequel nous allons utiliser des annotations pour les Tests Unitaires ! Souvenez-vous que les annotations peuvent porter sur différents éléments comme des classes, des méthodes, des interfaces, des constructeurs, des variables…
JUnit utilise des annotations personnalisées pour diriger l’exécution des tests.
Voici une liste non exhaustive des annotations utilisées :
L’API de réflection va permettre au moteur d’exécution de JUnit de définir le rôle de chaque méthode et de les exécuter au bon moment.
http://junit.org/junit/javadoc/4.5/index.html
Les 4 étapes correspondent à l’exécution des méthodes :
Illustration :
JUnit propose une API (un ensemble de méthodes permettant de réaliser des vérifications). La principale fonction est assert().
Au début du fichier de test, on importe les fonctions utilisées :
import org.junit.Test; import static org.junit.Assert.*;
Tous les tests fonctionnent à peu près de la même façon, on veut que dans un état donné, le code produise un résultat attendu. On compare donc le résultat attendu (expected) avec le résultat obtenu en appelant la méthode à tester (actual).
@Test(expected=java.lang.IndexOutOfBoundException.class)
Il faut donc d’abord initialiser les variables pour arriver à l’état que l’on souhaite (ce qui impose parfois de refactorer un peu le code à tester) avant de pouvoir lancer le test.
Les méthodes de test sont public, void et ne prennent pas de paramètres.
public
void
Il faut comparer un résultat de même type que celui produit par le retour de la méthode à tester :
@Test public void testGetPlusHauteDisponible() { // la méthode à tester renvoie un int Puissance4 app = new Puissance4(); char[][] plateau = { {' ','X',' ',' ',' ',' ', ' '}, {' ','O',' ',' ',' ',' ', ' '}, {' ','O',' ',' ',' ',' ', ' '}, {'O','X',' ',' ',' ',' ', ' '}, {'X','O',' ',' ',' ',' ', ' '}, {'O','X','X',' ',' ',' ', ' '} }; app.init(plateau); assertEquals(2, app.getPlusHauteDisponible(0)); // on commence à compter d'en haut assertEquals(4, app.getPlusHauteDisponible(2)); assertEquals(5, app.getPlusHauteDisponible(3)); }
@Test public void testColonnePleine() { // la méthode à tester renvoie un boolean Puissance4 app = new Puissance4(); char[][] plateau = { {' ','X',' ',' ',' ',' ', ' '}, {' ','O',' ',' ',' ',' ', ' '}, {' ','O',' ','O',' ',' ', ' '}, {'O','X','O',' ',' ',' ', ' '}, {'X','O',' ',' ',' ',' ', ' '}, {'O','X',' ',' ',' ',' ', ' '} }; app.init(plateau); assertFalse(app.colonnePleine(0)); assertTrue(app.colonnePleine(1)); }
Dans l’exemple précédent, on a couvert plusieurs cas dans une même méthode. On pourrait aussi faire deux méthodes de test séparées.
on utilise assertThrows pour attendre un type d’exception particulier.
assertThrows
@Test void testMinimumListeVide() { Assertions.assertThrows(NoSuchElementException.class, () -> { releveVide.min(); }); }
Autres exemples sur la documentation officielle
En général, les annotations sont placées devant la délcaration de l’élément qu’elle marque.
Les annotations permettent d’ajouter des metas informations sur des méthodes : https://www.javatpoint.com/java-annotation
Exemple de création d’une annotation TODO
Il en existe d’autres comme :
Exemple de l’utilisation de notre annoatation personnalisée :
Tout ça pour dire que JUnit utilise des annotations personnalisées pour diriger les tests !
L’api JUnit contient quelques annotations qu’il faut utiliser pour que les tests soient exécutés (expliqué plus haut) :
Autres annotations sur la documentation officielle.
JUnit n’est pas nativement intégrée dans java, il faut donc l’ajouter aux dépendances du projet.
On peut le faire directement dans les propriétés du projet, ou mieux dans le build.gradle :
dependencies { // Use JUnit Jupiter API for testing. testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' // Use JUnit Jupiter Engine for testing. testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' }
Pour exécuter une classe de test, il faut selectionner run as… junit test case.
On peut alors visualiser (eclipse, IntelliJ, …) les résultats graphiquement :
Les resultats (pour les non daltoniens) sont de 3 couleurs :
Si on clique sur un test, on obtient le détail (ce qui était attendu et ce que l’on a obtenu) :
La méthode TDD, c’est écrire les tests dès le début puis coder pour que les tests passent. Voici les étapes d’un cycle de développement en TDD :
Plus de détail ici.
Voici d’autres sources :
https://howtodoinjava.com/junit5/junit-5-assertions-examples/
exemples et exercices
Dans ce cas, vous avez créé un projet java avec Eclipse sans Maven ni Gradle et par conséquent vous devez ajouter la librairie JUnit au sein de votre projet. C’est simple et ça fonctionne tout seul avec la solution ci-dessous !
Si vous avez des erreurs sur le projet Ferme :
c’est parce qu’Eclipse ne reconnait pas JUnit pour l’éxécution, il faut l’ajouter dans la librairie du projet.
Solution: ajouter la librairie JUnit5 library au BuilPath du projet :
Click droit sur le projet puis build path -> configure buildpath -> add libraries -> add JUnit et c’est bon !
https://howtodoinjava.com/junit5/junit5-test-suites-examples/