Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 37

Gestion du cycle de vie des tests avec JUnit

Recommended resources

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 :

Ce diagramme montre le cycle de vie d'une classe de test JUnit. L'exécution commence par `@BeforeAll`, puis pour chaque test, la séquence `@BeforeEach`, `@Test`, `@AfterEach` est répétée. Finalement, `@AfterAll` est exécuté une seule fois à la fin.

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 @BeforeAll et @AfterAll doivent être static par 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 :

  1. Ouvrez votre classe ProductServiceTest.
  2. Déclarez un champ private ProductService service; au niveau de la classe.
  3. Créez une méthode setUp() annotée avec @BeforeEach.
  4. Déplacez l'initialisation service = new ProductService(); à l'intérieur de la méthode setUp().
  5. Supprimez la ligne ProductService service = new ProductService(); de chacune de vos méthodes de test (testFindExistingProduct, testFindUnavailableProduct, etc.).
  6. 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 @BeforeEach est utilisée pour initialiser le contexte (créer des objets, préparer des données) avant chaque test.
  • L'annotation @AfterEach est 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 @BeforeAll et @AfterAll sont utiles pour des configurations lourdes et partagées, mais @BeforeEach et @AfterEach sont 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.