Contactez-nous

Utilisation de `spring-boot-starter-test` (JUnit 5, Mockito, AssertJ, Spring Test)

Découvrez comment utiliser spring-boot-starter-test et ses composants clés (JUnit 5, Mockito, AssertJ, Spring Test) pour écrire des tests efficaces dans vos applications Spring Boot.

Le `spring-boot-starter-test` : votre boîte à outils essentielle pour les tests

Dans le développement logiciel moderne, et particulièrement avec un framework comme Spring Boot, les tests ne sont pas une option mais une nécessité. Ils garantissent la robustesse, la fiabilité et la maintenabilité de votre application. Spring Boot simplifie grandement la mise en place des tests en fournissant un "starter" dédié : `spring-boot-starter-test`.

Ce starter est le point d'entrée unique pour intégrer les bibliothèques de test les plus populaires et efficaces de l'écosystème Java. En ajoutant simplement cette dépendance à votre projet (généralement via Maven ou Gradle), vous disposez immédiatement d'un environnement de test complet et préconfiguré. Cela élimine le besoin de gérer individuellement les versions et les compatibilités de multiples bibliothèques, vous permettant de vous concentrer sur l'écriture de tests pertinents.

Le `spring-boot-starter-test` regroupe plusieurs dépendances clés, chacune jouant un rôle spécifique :

  • JUnit 5 : Le framework de test standard de facto pour Java, utilisé pour écrire et exécuter les cas de test.
  • Spring Test & Spring Boot Test : Fournit des fonctionnalités d'intégration spécifiques à Spring pour tester les composants et le contexte de l'application.
  • AssertJ : Une bibliothèque d'assertions fluides et très lisibles, améliorant l'expressivité de vos tests.
  • Mockito : Un framework de mocking puissant pour isoler les composants en simulant leurs dépendances.
  • Hamcrest : Une bibliothèque pour écrire des matchers d'objets (bien qu'AssertJ soit souvent préféré aujourd'hui).
  • JsonPath : Pour les assertions sur des contenus JSON.
  • JSONassert : Une autre bibliothèque d'assertions pour JSON.

Ensemble, ces outils offrent une base solide pour couvrir différents types de tests, des tests unitaires aux tests d'intégration.

JUnit 5 : structurer et exécuter vos tests

JUnit 5 est la pierre angulaire fournie par `spring-boot-starter-test`. Il sert de plateforme pour définir, organiser et exécuter vos tests. Son architecture modulaire (JUnit Platform, Jupiter, Vintage) permet une grande flexibilité. C'est le moteur Jupiter qui apporte les nouvelles annotations et fonctionnalités que vous utiliserez le plus souvent.

Les annotations de base de JUnit 5 sont intuitives et permettent de structurer clairement vos classes de test. `@Test` marque une méthode comme étant un cas de test individuel. `@BeforeEach` et `@AfterEach` définissent des méthodes à exécuter respectivement avant et après chaque méthode de test, idéales pour la mise en place et le nettoyage. `@BeforeAll` et `@AfterAll` font de même, mais une seule fois pour toute la classe de test (elles doivent être statiques par défaut). `@DisplayName` permet de donner un nom descriptif et lisible à vos tests et classes de test, améliorant la compréhension des rapports.

Voici un exemple simple d'une classe de test JUnit 5, sans dépendance Spring spécifique pour l'instant :

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalculateurTest {

    private Calculateur calculateur;

    @BeforeEach
    void setUp() {
        // Exécuté avant chaque test
        calculateur = new Calculateur();
        System.out.println("Initialisation du calculateur...");
    }

    @Test
    @DisplayName("Doit additionner deux nombres correctement")
    void testAddition() {
        int resultat = calculateur.additionner(3, 5);
        assertEquals(8, resultat, "L'addition de 3 et 5 devrait être 8");
    }

    @Test
    @DisplayName("Doit soustraire deux nombres correctement")
    void testSoustraction() {
        int resultat = calculateur.soustraire(10, 4);
        assertEquals(6, resultat, "La soustraction de 4 à 10 devrait être 6");
    }

    // Supposons que Calculateur est une classe simple :
    static class Calculateur {
        int additionner(int a, int b) { return a + b; }
        int soustraire(int a, int b) { return a - b; }
    }
}

Cet exemple montre comment utiliser les annotations de base et les assertions standard de JUnit (`assertEquals`).

Mockito : isoler vos composants avec les mocks

Lors de l'écriture de tests unitaires, l'objectif est de tester un composant (une classe, une méthode) de manière isolée, sans dépendre du comportement réel de ses collaborateurs (autres classes dont il dépend). C'est là qu'intervient Mockito. Il permet de créer des objets factices, appelés "mocks", qui simulent le comportement des dépendances réelles.

Spring Boot facilite l'intégration de Mockito grâce à l'annotation `@MockBean`. Lorsque vous utilisez `@SpringBootTest` ou une annotation de test de slice (comme `@WebMvcTest`, `@DataJpaTest`), vous pouvez déclarer un champ avec `@MockBean`. Spring remplacera alors le bean réel de ce type dans le contexte d'application par un mock Mockito. Vous pouvez ensuite définir le comportement attendu de ce mock à l'aide des méthodes de Mockito (`when`, `thenReturn`, `verify`, etc.).

Imaginons un service `CommandeService` qui dépend d'un `StockRepository` pour vérifier la disponibilité d'un produit :

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.junit.jupiter.api.Assertions.assertTrue;

// Exemple SANS intégration Spring, juste Mockito + JUnit
@ExtendWith(MockitoExtension.class) // Active les annotations Mockito
class CommandeServiceTest {

    @Mock // Crée un mock pour cette dépendance
    private StockRepository stockRepository;

    @InjectMocks // Crée une instance de CommandeService et injecte les mocks (@Mock)
    private CommandeService commandeService;

    @Test
    void passerCommande_devraitReussir_siStockSuffisant() {
        // Arrange: Définir le comportement du mock
        String produitId = "PROD123";
        int quantiteDemandee = 5;
        when(stockRepository.verifierDisponibilite(produitId, quantiteDemandee)).thenReturn(true);

        // Act: Appeler la méthode à tester
        boolean resultat = commandeService.passerCommande(produitId, quantiteDemandee);

        // Assert: Vérifier le résultat et les interactions avec le mock
        assertTrue(resultat, "La commande aurait dû réussir.");
        verify(stockRepository).verifierDisponibilite(produitId, quantiteDemandee); // Vérifie que la méthode du mock a été appelée
    }

    // Classes factices pour l'exemple
    interface StockRepository { boolean verifierDisponibilite(String produitId, int quantite); }
    static class CommandeService {
        private StockRepository repository;
        public CommandeService(StockRepository repository) { this.repository = repository; }
        public boolean passerCommande(String pId, int qte) { return repository.verifierDisponibilite(pId, qte); /* + logique métier */ }
    }
}

Dans un contexte Spring Boot avec `@SpringBootTest` ou similaire, on utiliserait `@MockBean` à la place de `@Mock` et `@InjectMocks`, et Spring gérerait l'injection.

AssertJ : écrire des assertions claires et puissantes

Les assertions sont au coeur de tout test : elles vérifient que le comportement réel du code correspond au comportement attendu. Si JUnit fournit des méthodes d'assertions de base (`assertEquals`, `assertTrue`, etc.), AssertJ offre une alternative beaucoup plus riche et expressive.

L'avantage principal d'AssertJ réside dans son API fluide (fluent API) et sa lisibilité proche du langage naturel. Les assertions commencent typiquement par `assertThat(valeurActuelle)...` suivi d'une série de méthodes d'assertion spécifiques au type de la valeur. Cela rend les tests plus faciles à lire et à comprendre, surtout pour des vérifications complexes, notamment sur les collections, les chaînes de caractères ou les exceptions.

Comparons une assertion JUnit standard avec son équivalent AssertJ pour vérifier le contenu d'une liste :

import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;

// Import statique pour AssertJ pour alléger l'écriture
import static org.assertj.core.api.Assertions.assertThat;

class AssertJExampleTest {

    @Test
    void verificationListeAvecAssertJ() {
        List prenoms = Arrays.asList("Alice", "Bob", "Charlie");

        // Avec AssertJ
        assertThat(prenoms)
            .isNotNull()                // Vérifie que la liste n'est pas nulle
            .hasSize(3)                 // Vérifie la taille
            .contains("Bob", "Alice") // Vérifie la présence d'éléments (ordre indifférent)
            .doesNotContain("David")    // Vérifie l'absence d'un élément
            .startsWith("Alice")        // Vérifie le premier élément
            .endsWith("Charlie")        // Vérifie le dernier élément
            .containsExactly("Alice", "Bob", "Charlie"); // Vérifie tous les éléments dans l'ordre exact
    }

    @Test
    void verificationProprietesObjetAvecAssertJ() {
        Utilisateur utilisateur = new Utilisateur("Dupont", 30);

        assertThat(utilisateur)
            .isNotNull()
            .extracting(Utilisateur::getNom, Utilisateur::getAge) // Extrait les propriétés
            .containsExactly("Dupont", 30); // Vérifie les valeurs extraites

        // Ou assertion sur les propriétés individuellement
        assertThat(utilisateur.getNom()).isEqualTo("Dupont");
        assertThat(utilisateur.getAge()).isGreaterThan(25).isLessThan(35);
    }

    // Classe factice pour l'exemple
    static class Utilisateur {
        private String nom;
        private int age;
        public Utilisateur(String nom, int age) { this.nom = nom; this.age = age; }
        public String getNom() { return nom; }
        public int getAge() { return age; }
    }
}

La lisibilité et la richesse des méthodes d'assertion proposées par AssertJ en font un choix privilégié dans la plupart des projets Spring Boot modernes.

Spring Test et Spring Boot Test : intégrer le framework dans vos tests

Au-delà des tests unitaires purs qui isolent les composants, il est souvent nécessaire d'écrire des tests d'intégration. Ces tests vérifient la collaboration entre plusieurs composants ou même le fonctionnement d'une partie significative de l'application au sein du contexte Spring.

Le module `spring-test` fournit les fondations pour ces tests d'intégration. Il permet notamment de charger et de gérer le contexte d'application Spring (`ApplicationContext`) pendant l'exécution des tests. Des annotations comme `@ContextConfiguration` (pour spécifier comment charger le contexte) et `@Autowired` (pour injecter des beans du contexte dans les tests) en sont des éléments clés.

Spring Boot simplifie encore davantage cela avec le module `spring-boot-test`. L'annotation principale est `@SpringBootTest`. Utilisée sur une classe de test, elle indique à Spring Boot de rechercher la configuration principale (`@SpringBootApplication` ou `@SpringBootConfiguration`) et de démarrer un contexte d'application complet (ou partiel, selon les options) pour le test. Cela permet de tester l'application de manière très proche de son fonctionnement réel.

Voici un exemple simple d'utilisation de `@SpringBootTest` pour tester un service qui serait un bean Spring :

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Service;

import static org.assertj.core.api.Assertions.assertThat;

// L'annotation @SpringBootTest charge le contexte de l'application
@SpringBootTest
class MonServiceIntegrationTest {

    // Injecte le bean MonService depuis le contexte Spring chargé
    @Autowired
    private MonService monService;

    @Test
    void serviceDevraitRetournerMessageCorrect() {
        String message = monService.genererMessage("Monde");
        assertThat(message).isEqualTo("Bonjour, Monde!");
    }\n
    // Supposons que MonService est défini quelque part dans votre application
    // @Service
    // public static class MonService {
    //     public String genererMessage(String nom) {
    //         return "Bonjour, " + nom + "!";
    //     }
    // }
    // Et qu'une classe @SpringBootApplication existe pour que @SpringBootTest la trouve.
}

// Définition du service pour que l'exemple soit autonome (dans un vrai projet, ce serait dans src/main/java)
@Service
class MonService {
    public String genererMessage(String nom) {
        return "Bonjour, " + nom + "!";
    }
}

Dans cet exemple, `@SpringBootTest` charge le contexte, trouve le bean `MonService` (car il est annoté `@Service` et scanné par l'application), et `@Autowired` l'injecte dans la classe de test. Le test peut alors appeler directement les méthodes du service comme s'il était utilisé dans l'application réelle.