Skip to main content

Welcome to a Grasp lesson

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

Lesson 76

Tests d'intégration des API Spring Boot

Recommended resources

Bonjour ! Dans la leçon précédente, nous avons mis en place une couche d'autorisation robuste avec des rôles, nous permettant de définir précisément qui a le droit d'accéder à nos endpoints d'API. Notre application est maintenant fonctionnelle et sécurisée. Il est temps de garantir sa qualité sur le long terme.

Aujourd'hui, nous allons aborder une compétence fondamentale pour tout développeur professionnel : les tests d'intégration. L'objectif est d'apprendre à écrire des tests automatisés qui valident le bon fonctionnement de notre API de bout en bout, en s'assurant que toutes les couches (contrôleur, service, repository, base de données) collaborent comme prévu. Nous utiliserons pour cela les puissants outils fournis par Spring Boot Test.

1. Tests d'Intégration vs. Tests Unitaires

Avant de plonger dans le code, il est crucial de bien distinguer deux types de tests.

  • Tests unitaires : Ils se concentrent sur une seule "unité" de code (une méthode, une classe) de manière isolée. Toutes les dépendances externes (comme la base de données ou d'autres services) sont "mockées", c'est-à-dire remplacées par des objets factices. L'objectif est de vérifier la logique interne d'un composant, indépendamment du reste du système.
  • Tests d'intégration : Ils vérifient que plusieurs composants de l'application fonctionnent correctement ensemble. Au lieu de mocker les dépendances, on utilise des versions réelles ou quasi-réelles (par exemple, une vraie base de données qui tourne dans un conteneur). Ces tests sont plus lents mais beaucoup plus proches des conditions réelles d'utilisation.

L'analogie de la voiture est souvent utilisée : le test unitaire consiste à tester le moteur sur un banc d'essai, tandis que le test d'intégration consiste à démarrer la voiture pour vérifier que le moteur, la transmission et les roues fonctionnent ensemble.

Le diagramme ci-dessous illustre l'architecture d'un test d'intégration dans Spring Boot. On voit qu'un contexte d'application complet est chargé, avec un vrai serveur web et les différents Beans, simulant ainsi l'application réelle.

Ce diagramme montre comment un test d'intégration charge un contexte d'application Spring complet, incluant un serveur web et les différents composants (Controller, Service, etc.), pour tester leurs interactions de manière réaliste.

L'article suivant, bien que centré sur le "test slicing", propose une excellente introduction qui distingue les tests unitaires par couche (@WebMvcTest, @DataMongoTest) des tests d'intégration complets (@SpringBootTest).

Tests unitaires et d'intégration avec Spring Boot : le “test slicing”

Cet article d'Effective Coding clarifie les différentes stratégies de test dans Spring Boot.

Lisez l'introduction et les sections "Scénario 2 : Test unitaire brique DATA" et "Scénario 3 : Test d’intégration". Notez la différence fondamentale entre @DataMongoTest (qui ne charge que la couche de persistance) et @SpringBootTest (qui charge tout le contexte de l'application). C'est ce dernier que nous allons utiliser.


2. Mise en Place de l'Environnement de Test

Pour écrire nos tests d'intégration, nous devons configurer notre projet pour qu'il utilise un environnement de test isolé, notamment une base de données distincte de celle de production.

Étape 1 : Ajouter les dépendances Maven

Votre projet Spring Boot inclut déjà spring-boot-starter-test, qui contient JUnit 5, Mockito et les utilitaires de base. Nous allons ajouter une base de données en mémoire, H2, qui sera utilisée uniquement pendant les tests.

Ouvrez votre pom.xml et ajoutez cette dépendance :

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

Le scope test garantit que cette dépendance ne sera pas incluse dans le package final de votre application.

Étape 2 : Configurer une base de données de test

Créez un fichier de configuration spécifique pour les tests dans src/test/resources/application.properties. Spring Boot chargera automatiquement ce fichier lors de l'exécution des tests, surchargeant la configuration principale.

src/test/resources/application.properties




# Configuration de la base de données H2 en mémoire pour les tests
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=




# Configuration JPA pour H2
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
  • jdbc:h2:mem:testdb : Crée une base de données en mémoire nommée testdb.
  • spring.jpa.hibernate.ddl-auto=create-drop : Demande à Hibernate de créer le schéma de la base de données au début des tests et de le détruire à la fin. Chaque exécution de test partira d'une base de données propre.

Étape 3 : La classe de test et ses annotations

Créons une classe de test pour notre UserController.

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {
    // ... nos tests
}
  • @SpringBootTest : C'est l'annotation maîtresse. Elle demande à Spring Boot de charger le contexte complet de l'application, comme si elle démarrait normalement.
  • @AutoConfigureMockMvc : Configure MockMvc, un outil puissant qui permet de simuler des requêtes HTTP vers nos contrôleurs sans passer par une vraie couche réseau, tout en exécutant la chaîne de traitement complète de Spring (filtres, intercepteurs, etc.).

3. Écrire notre Premier Test d'Intégration avec MockMvc

Nous allons maintenant écrire un test pour l'endpoint qui liste tous les utilisateurs (GET /api/users).

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        // Nettoyer la base avant chaque test
        userRepository.deleteAll();
    }

    @Test
    void shouldGetAllUsers() throws Exception {
        // Arrange : préparer les données
        User user1 = new User("user1", "user1@test.com", "password");
        User user2 = new User("user2", "user2@test.com", "password");
        userRepository.saveAll(List.of(user1, user2));

        // Act & Assert : exécuter la requête et vérifier le résultat
        mockMvc.perform(get("/api/users"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.size()").value(2))
                .andExpect(jsonPath("$[0].username").value("user1"));
    }
}

La vidéo suivante explique très bien comment tester un contrôleur avec MockMvc. Même si elle utilise @WebMvcTest (un test de "slice"), les techniques avec MockMvc sont identiques à celles que nous utilisons dans notre test d'intégration complet.

Spring Boot Testing : Rédiger des Tests Unitaires en utilisant JUnit 5 - Mockito - Jacoco

Cette vidéo de Coding With Armand montre en détail l'utilisation de MockMvc pour tester un contrôleur.

Regardez attentivement la section sur le test du contrôleur (PersonController test). Concentrez-vous sur : L'injection de MockMvc. L'utilisation de mockMvc.perform() pour simuler les requêtes GET, POST, PUT. L'enchaînement des andExpect() pour vérifier le statut de la réponse (status().isOk()) et le contenu du corps JSON avec jsonPath().

Décortiquons notre test :

  1. Arrange : On prépare l'état de notre système. Ici, on insère deux utilisateurs dans notre base de données de test (H2).
  2. Act : On exécute l'action à tester avec mockMvc.perform(get("/api/users")).
  3. Assert : On vérifie que le résultat est correct à l'aide de andExpect() :
    • status().isOk() : Le code de statut HTTP est 200.
    • content().contentType(...) : Le type de contenu de la réponse est bien du JSON.
    • jsonPath("$.size()") : jsonPath est un langage d'expression pour interroger des documents JSON. $ représente la racine du document. Ici, on vérifie que le tableau JSON retourné a une taille de 2.
    • jsonPath("$[0].username") : On vérifie que le champ username du premier objet ([0]) du tableau est bien "user1".

4. Tester les Endpoints Sécurisés

Nos tests actuels vont probablement échouer si les endpoints sont protégés. En effet, MockMvc envoie par défaut des requêtes anonymes. Nous devons simuler un utilisateur authentifié. Pour cela, le module spring-security-test est indispensable.

Assurez-vous d'avoir cette dépendance dans votre pom.xml (avec la portée test) :

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

Nous pouvons maintenant utiliser l'annotation @WithMockUser sur nos méthodes de test.

Imaginons que notre endpoint GET /api/users soit réservé aux administrateurs, comme nous l'avons configuré dans la leçon précédente avec @PreAuthorize("hasRole('ADMIN')").

Test 1 : Accès non authentifié (doit échouer)

@Test
void shouldReturnForbiddenWhenGettingAllUsersWithoutAuth() throws Exception {
    mockMvc.perform(get("/api/users"))
            .andExpect(status().isForbidden()); // Ou isUnauthorized() selon la configuration
}

Test 2 : Accès avec le mauvais rôle (doit échouer)

@Test
@WithMockUser(roles = "USER")
void shouldReturnForbiddenWhenGettingAllUsersWithUserRole() throws Exception {
    mockMvc.perform(get("/api/users"))
            .andExpect(status().isForbidden());
}

Test 3 : Accès avec le bon rôle (doit réussir)

@Test
@WithMockUser(roles = "ADMIN")
void shouldReturnAllUsersWhenUserIsAdmin() throws Exception {
    // ... même code que notre premier test (Arrange, Act, Assert)
    mockMvc.perform(get("/api/users"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.size()").value(2));
}

Avec ces trois tests, nous avons une couverture complète du comportement de sécurité de notre endpoint. L'image ci-dessous montre à quoi ressemble une suite de tests d'intégration réussie dans IntelliJ.

Exemple d'une classe de test d'intégration pour un `UserController` exécutée avec succès dans l'IDE, montrant la validation de plusieurs scénarios (création, lecture, mise à jour, suppression) et la structure de la classe de test.

Pour un exemple complet et très bien structuré d'une classe de test d'intégration utilisant les mêmes principes, vous pouvez consulter le code dans l'article suivant.

Tests d'intégration avec Springboot, Docker et Testcontainers

Cet article de Younup montre une implémentation professionnelle de tests d'intégration.

Examinez la classe de test finale (BookPostgresSQLTest). Elle combine @SpringBootTest, @AutoConfigureMockMvc, MockMvc et le nettoyage de la base de données avec @BeforeEach, appliquant exactement les concepts que nous venons de voir.


Conclusion

Félicitations ! Vous venez d'acquérir une compétence essentielle pour garantir la robustesse et la maintenabilité de vos applications backend. Les tests d'intégration vous donnent la confiance nécessaire pour faire évoluer votre code sans introduire de régressions.

Points clés à retenir :

  • Les tests d'intégration valident la collaboration entre plusieurs composants, contrairement aux tests unitaires qui les isolent.
  • L'annotation @SpringBootTest est la clé pour lancer un contexte d'application complet.
  • @AutoConfigureMockMvc et l'objet MockMvc permettent de simuler des requêtes HTTP de manière simple et efficace.
  • Il est crucial d'utiliser une base de données de test (comme H2) et de la configurer dans src/test/resources/application.properties.
  • L'expression jsonPath est un outil puissant pour valider le contenu des réponses JSON.
  • Le module spring-security-test et l'annotation @WithMockUser permettent de tester facilement les endpoints sécurisés en simulant des utilisateurs avec des rôles spécifiques.

Dans la prochaine et dernière leçon de ce module, nous allons nous concentrer sur la documentation de notre API. Une API bien testée est une chose, mais une API bien documentée est essentielle pour qu'elle puisse être utilisée par d'autres développeurs ou des applications frontend. Nous explorerons des outils comme Postman ou Swagger pour générer une documentation claire et interactive.