Bonjour ! Dans la leçon précédente, vous avez appris à utiliser une panoplie d'assertions JUnit (assertTrue, assertNotNull, etc.) pour valider le comportement de votre code de manière précise. Vous savez maintenant comment vérifier si un résultat est correct.
Aujourd'hui, nous allons aborder une autre pierre angulaire des tests unitaires professionnels : le cycle de vie des tests. L'objectif est de vous apprendre à utiliser les annotations @BeforeEach et @AfterEach pour garantir que chaque test s'exécute dans un environnement propre et isolé. C'est une pratique essentielle pour écrire des tests fiables, maintenables et qui ne s'influencent pas mutuellement.
1. Le problème : l'indépendance des tests
Imaginez que vous écriviez plusieurs tests pour une même classe. Si un test modifie l'état d'un objet (par exemple, ajoute un élément à une liste) et que le test suivant utilise ce même objet, le second test pourrait échouer (ou réussir par erreur !) à cause des actions du premier. C'est ce qu'on appelle une dépendance entre les tests, et c'est une source majeure de problèmes.
Les tests unitaires doivent être :
- Indépendants : L'échec ou la réussite d'un test ne doit jamais dépendre d'un autre.
- Répétables : Un test doit donner le même résultat à chaque exécution, quel que soit l'ordre dans lequel il est lancé.
Pour garantir cela, chaque test doit commencer avec un contexte connu et propre. De plus, on observe souvent du code de préparation (initialisation d'objets) qui se répète au début de chaque méthode de test. Comment pouvons-nous gérer cela efficacement ? La réponse se trouve dans les annotations de cycle de vie de JUnit.
2. Le cycle de vie d'un test JUnit
JUnit exécute une série d'étapes bien définies pour chaque test. On peut s'accrocher à certaines de ces étapes pour exécuter notre propre code de préparation (setup) ou de nettoyage (teardown).
Les annotations les plus importantes pour gérer ce cycle sont :
@BeforeEach: Exécute une méthode avant chaque méthode de test.@AfterEach: Exécute une méthode après chaque méthode de test.@BeforeAll: Exécute une méthode une seule fois, avant tous les tests de la classe.@AfterAll: Exécute une méthode une seule fois, après tous les tests de la classe.
Le diagramme suivant illustre ce flux d'exécution :

Dans cette leçon, nous nous concentrerons sur @BeforeEach et @AfterEach, qui sont fondamentaux pour garantir l'isolation de chaque test.
3. Préparer et nettoyer avec @BeforeEach et @AfterEach
La vidéo suivante explique et démontre parfaitement l'utilité de ces annotations avec un exemple très pertinent pour un développeur backend.
TUTO Java : Mise en œuvre de tests unitaires avec JUnit 5
Cette vidéo de Dominique Liard, que vous avez déjà consultée, aborde maintenant la gestion du cycle de vie des tests.
Regardez attentivement la section qui explique la préparation et le nettoyage. L'auteur y explique : L'utilité des méthodes de setup (préparation) et teardown (nettoyage). L'importance des annotations @BeforeEach et @AfterEach pour garantir l'indépendance des tests. Un exemple concret avec une connexion à une base de données, montrant comment ouvrir une connexion propre avant chaque test et la fermer après, évitant ainsi les interférences. Une démonstration via des affichages console qui prouve l'ordre d'exécution.
Centraliser la préparation avec @BeforeEach
Reprenons l'exemple du ProductService de la leçon précédente. Dans chaque méthode de test, vous deviez écrire ProductService service = new ProductService();. C'est répétitif. On peut améliorer cela.
Avant (répétitif) :
class ProductServiceTest {
@Test
void testFindExistingProduct() {
ProductService service = new ProductService();
Product product = service.findById(1L);
assertNotNull(product);
}
@Test
void testFindNonExistingProduct() {
ProductService service = new ProductService();
Product product = service.findById(99L);
assertNull(product);
}
}
Après (factorisé et propre) :
class ProductServiceTest {
private ProductService service; // 1. Déclarer la variable d'instance
@BeforeEach // 2. Annoter la méthode de préparation
void setUp() {
// 3. Cette méthode s'exécute avant chaque test
System.out.println("Initialisation du service...");
service = new ProductService();
}
@Test
void testFindExistingProduct() {
// Le champ 'service' est déjà initialisé ici !
Product product = service.findById(1L);
assertNotNull(product);
}
@Test
void testFindNonExistingProduct() {
// Une NOUVELLE instance de service a été créée pour ce test aussi
Product product = service.findById(99L);
assertNull(product);
}
}
Avec cette structure, chaque test est garanti de démarrer avec une instance fraîche du ProductService, éliminant tout risque d'interférence.
Nettoyer les ressources avec @AfterEach
La méthode annotée avec @AfterEach est idéale pour libérer les ressources utilisées pendant un test, comme fermer une connexion à une base de données, supprimer des fichiers temporaires, etc. Elle s'exécute même si le test échoue, garantissant un nettoyage systématique.
class FileProcessingTest {
private Path tempFile;
@BeforeEach
void setUp() throws IOException {
// Crée un fichier temporaire avant chaque test
tempFile = Files.createTempFile("test-", ".txt");
}
@AfterEach
void tearDown() throws IOException {
// Supprime le fichier temporaire après chaque test
Files.deleteIfExists(tempFile);
}
@Test
void testFileWriting() {
// ... logique de test qui écrit dans tempFile ...
}
}
4. Approfondir la gestion du contexte
Pour une lecture plus détaillée et un exemple complet, l'article suivant est une excellente ressource. Il met en pratique ces concepts sur une classe de liste chaînée, un cas d'usage classique et très formateur.
Nos premiers tests - Les tests unitaires en Java
Cet article du site Zeste de Savoir explique comment gérer proprement le contexte d'un test.
Lisez la section "Tester proprement : la gestion du contexte". Concentrez-vous sur : Le tableau qui résume le rôle des quatre annotations de cycle de vie. L'exemple de code qui montre comment initialiser une liste avec des données avant chaque test (@BeforeEach) et la vider après (@AfterEach). L'explication de comment tester une méthode void (qui ne retourne rien) en vérifiant le changement d'état de l'objet après l'appel de la méthode.
5. ...Each vs ...All : Quand utiliser quoi ?
Vous avez vu les quatre annotations. Voici une règle simple pour choisir :
@BeforeEach/@AfterEach: À utiliser dans 95% des cas. C'est la garantie d'une isolation totale entre les tests. Chaque test a son propre environnement frais.@BeforeAll/@AfterAll: À réserver pour des opérations de préparation/nettoyage très coûteuses qui peuvent être partagées entre tous les tests sans risque. Par exemple : démarrer un serveur de base de données en mémoire une seule fois pour toute la classe de test. Attention : les méthodes annotées avec@BeforeAllet@AfterAlldoivent êtrestaticpar défaut.
6. Exercice : Refactorisez vos tests de produits
Il est temps de mettre en pratique. Reprenez l'exercice de la leçon précédente sur ProductServiceTest.
Votre mission :
- Ouvrez votre classe
ProductServiceTest. - Déclarez un champ
private ProductService service;au niveau de la classe. - Créez une méthode
setUp()annotée avec@BeforeEach. - Déplacez l'initialisation
service = new ProductService();à l'intérieur de la méthodesetUp(). - Supprimez la ligne
ProductService service = new ProductService();de chacune de vos méthodes de test (testFindExistingProduct,testFindUnavailableProduct, etc.). - Lancez à nouveau tous vos tests. Ils doivent tous passer, prouvant que votre refactoring a réussi et que chaque test reçoit bien son instance de
ProductService.
Cet exercice simple mais fondamental vous fera intégrer le réflexe de factoriser la préparation des tests, une compétence clé pour un code de test propre.
Conclusion
Vous maîtrisez désormais une technique essentielle pour l'écriture de tests unitaires robustes. En isolant chaque test, vous vous assurez que vos suites de tests sont fiables et que les erreurs sont plus faciles à diagnostiquer.
Les points clés à retenir :
- Les tests doivent être indépendants les uns des autres pour être fiables.
- L'annotation
@BeforeEachest utilisée pour initialiser le contexte (créer des objets, préparer des données) avant chaque test. - L'annotation
@AfterEachest utilisée pour nettoyer les ressources (fermer des connexions, supprimer des fichiers) après chaque test. - Ce cycle Setup → Test → Teardown garantit qu'un test ne laisse pas d'artefacts qui pourraient perturber le suivant.
- Les annotations
@BeforeAllet@AfterAllsont utiles pour des configurations lourdes et partagées, mais@BeforeEachet@AfterEachsont la norme pour l'isolation.
Dans la prochaine leçon, nous compléterons notre arsenal de test en apprenant à vérifier les cas d'erreur. Vous découvrirez comment utiliser assertThrows pour vous assurer que votre code lève bien les exceptions attendues lorsqu'il est confronté à des données invalides.