
Tests d'intégration avec `@SpringBootTest`
Apprenez à utiliser l'annotation @SpringBootTest pour charger le contexte d'application Spring Boot et effectuer des tests d'intégration robustes.
Introduction aux tests d'intégration avec @SpringBootTest
Alors que les tests unitaires se concentrent sur l'isolation des composants, les tests d'intégration visent à vérifier la collaboration entre différentes parties de votre application Spring Boot. Ils s'assurent que les couches (contrôleurs, services, repositories) interagissent correctement, souvent en impliquant le contexte d'application Spring lui-même. L'annotation `@SpringBootTest` est l'outil principal fourni par Spring Boot pour réaliser ce type de tests.
L'objectif principal de `@SpringBootTest` est de démarrer un `ApplicationContext` Spring pour votre test. Par défaut, il recherche une classe annotée avec `@SpringBootConfiguration` (qui est incluse dans `@SpringBootApplication`) dans votre projet et l'utilise pour charger la configuration complète de votre application. Cela signifie que tous vos beans (services, repositories, contrôleurs, etc.) sont créés et injectés comme ils le seraient lorsque l'application s'exécute normalement.
Cette approche permet de tester des flux fonctionnels complets ou des interactions complexes entre différents beans. C'est un outil puissant mais qui peut aussi rendre les tests plus lents car le démarrage du contexte Spring prend du temps. Il est donc crucial de l'utiliser à bon escient, généralement pour des scénarios qui ne peuvent pas être couverts efficacement par des tests unitaires ou des tests de "slice" plus ciblés.
Utilisation de base et injection de beans
L'utilisation la plus simple de `@SpringBootTest` consiste à annoter votre classe de test avec. Combiné avec JUnit 5 (via `@ExtendWith(SpringExtension.class)`, bien que souvent implicite avec `@SpringBootTest`), cela suffit à charger le contexte.
Une fois le contexte chargé, vous pouvez injecter n'importe quel bean géré par Spring directement dans votre classe de test en utilisant `@Autowired`. Cela vous permet d'interagir avec vos services, repositories ou autres composants comme s'ils étaient utilisés par l'application elle-même.
Voici un exemple concret testant l'interaction entre un service et un repository (en supposant qu'ils sont définis comme des beans 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.Repository;
import org.springframework.stereotype.Service;
import static org.assertj.core.api.Assertions.assertThat;
// Charge le contexte complet de l'application Spring Boot
@SpringBootTest
class UtilisateurServiceIntegrationTest {
// Injecte les beans réels depuis le contexte chargé
@Autowired
private UtilisateurService utilisateurService;
@Autowired
private UtilisateurRepository utilisateurRepository; // Supposons une interface JpaRepository
@Test
void creerUtilisateur_devraitPersisterUtilisateur() {
// Arrange
String nomUtilisateur = "testuser";
// Act
Utilisateur nouvelUtilisateur = utilisateurService.creerUtilisateur(nomUtilisateur);
// Assert
assertThat(nouvelUtilisateur).isNotNull();
assertThat(nouvelUtilisateur.getId()).isNotNull(); // L'ID devrait être généré par la persistance
assertThat(nouvelUtilisateur.getNom()).isEqualTo(nomUtilisateur);
// Vérification supplémentaire directement via le repository (possible car c'est un test d'intégration)
Utilisateur trouve = utilisateurRepository.findById(nouvelUtilisateur.getId()).orElse(null);
assertThat(trouve).isNotNull();
assertThat(trouve.getNom()).isEqualTo(nomUtilisateur);
}
}
// --- Définitions des classes (normalement dans src/main/java) ---
// Entité JPA (simplifiée)
// @Entity
class Utilisateur {
// @Id @GeneratedValue
private Long id;
private String nom;
// Constructeurs, Getters, Setters...
public Utilisateur(String nom) { this.nom = nom; }
public Long getId() { return id; } public void setId(Long id) { this.id = id; } // Simule la génération d'ID
public String getNom() { return nom; }
}
// Interface Repository Spring Data JPA (simplifiée)
// @Repository
interface UtilisateurRepository /* extends JpaRepository */ {
// Simule la sauvegarde et la recherche pour l'exemple autonome
java.util.Map db = new java.util.HashMap<>();
java.util.concurrent.atomic.AtomicLong counter = new java.util.concurrent.atomic.AtomicLong();
default Utilisateur save(Utilisateur u) {
if(u.getId() == null) u.setId(counter.incrementAndGet());
db.put(u.getId(), u);
return u;
}
default java.util.Optional findById(Long id) { return java.util.Optional.ofNullable(db.get(id)); }
}
// Service Spring
@Service
class UtilisateurService {
private final UtilisateurRepository repository;
@Autowired
public UtilisateurService(UtilisateurRepository repository) {
this.repository = repository;
}
public Utilisateur creerUtilisateur(String nom) {
Utilisateur u = new Utilisateur(nom);
return repository.save(u);
}
}
// Classe de configuration minimale pour que @SpringBootTest fonctionne (normalement @SpringBootApplication)
@org.springframework.boot.SpringBootConfiguration
@org.springframework.context.annotation.ComponentScan // Pour trouver les @Service, @Repository
class TestApplication {}
// Implémentation factice du repository pour l'exemple
@Repository
class FakeUtilisateurRepositoryImpl implements UtilisateurRepository {}
Dans cet exemple, `@SpringBootTest` charge le contexte qui contient `UtilisateurService` et `UtilisateurRepository`. Le test injecte ces beans et vérifie que l'appel à `creerUtilisateur` persiste bien l'entité, ce qui est validé en la récupérant ensuite via le repository.
Configuration fine du contexte de test
Il n'est pas toujours souhaitable ou nécessaire de charger l'intégralité de la configuration de l'application pour chaque test d'intégration. `@SpringBootTest` offre plusieurs attributs pour affiner le contexte chargé :
- `classes` : Permet de spécifier explicitement les classes de configuration (`@Configuration`, `@SpringBootConfiguration`) à utiliser pour créer le contexte. C'est utile pour ne charger qu'un sous-ensemble de votre application ou pour utiliser une configuration spécifique au test.
@SpringBootTest(classes = { MonService.class, MonRepositoryConfig.class }) class MonServiceTestAvecConfigSpecifique { ... } - `properties` : Permet de définir ou de surcharger des propriétés d'application directement dans l'annotation, sous forme de paires clé=valeur. Utile pour des ajustements simples.
@SpringBootTest(properties = { "mon.service.url=http://localhost:8888", "spring.jpa.show-sql=false" }) class TestAvecProprietesSurchargees { ... }
Pour une gestion plus avancée des propriétés, l'annotation `@TestPropertySource` est souvent préférée. Elle permet de charger des propriétés depuis des fichiers spécifiques (`.properties` ou `.yml`) ou de les définir inline de manière plus structurée que l'attribut `properties`.
Enfin, l'annotation `@ActiveProfiles("test")` permet d'activer un ou plusieurs profils Spring spécifiques pour l'exécution du test. Cela permet de charger des configurations ou des beans conditionnels définis avec `@Profile("test")`, comme une base de données en mémoire H2 au lieu de la base de données de production.
Tester l'environnement web
Une utilisation fréquente de `@SpringBootTest` est de tester la couche web de l'application, incluant les contrôleurs, la sérialisation/désérialisation, etc. L'attribut `webEnvironment` de `@SpringBootTest` contrôle la manière dont l'environnement web est configuré :
- `WebEnvironment.MOCK` (défaut) : Charge un contexte web (`WebApplicationContext`) mais sans démarrer de serveur d'application réel. Il fournit un environnement de servlet simulé (mock). C'est idéal pour tester les contrôleurs MVC en utilisant `MockMvc`, qui permet de simuler des requêtes HTTP sans la surcharge réseau. `MockMvc` peut être auto-injecté dans ce mode.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) @AutoConfigureMockMvc // Nécessaire pour injecter MockMvc class MonControllerMockMvcTest { @Autowired private MockMvc mockMvc; // ... tests avec mockMvc.perform(...) ... } - `WebEnvironment.RANDOM_PORT` : Charge un `WebApplicationContext` et démarre un serveur embarqué (Tomcat, Jetty...) sur un port disponible aléatoire. L'application est réellement accessible via HTTP pendant le test. C'est utile pour des tests end-to-end ou pour tester le client HTTP. Le port utilisé est injectable via `@LocalServerPort`.
- `WebEnvironment.DEFINED_PORT` : Similaire à `RANDOM_PORT`, mais démarre le serveur sur un port fixe défini par la propriété `server.port` (ou le port par défaut 8080). Moins courant car peut causer des conflits si le port est déjà utilisé.
- `WebEnvironment.NONE` : Ne charge pas d'environnement web du tout. Le contexte sera un `ApplicationContext` standard, non-web. Utile si votre application n'a pas de partie web ou si vous voulez tester uniquement la logique métier sans le contexte web.
Lorsque vous utilisez `RANDOM_PORT` ou `DEFINED_PORT`, vous pouvez injecter `TestRestTemplate` (pour les appels REST synchrones classiques) ou `WebTestClient` (pour les appels synchrones ou asynchrones/réactifs) afin d'envoyer de véritables requêtes HTTP à votre application en cours d'exécution durant le test.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MonControllerEndToEndTest {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
@Test
void getEndpoint_devraitRetournerOk() {
String url = "http://localhost:" + port + "/api/mon-endpoint";
ResponseEntity response = restTemplate.getForEntity(url, String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
} Combiner @SpringBootTest avec @MockBean
Même dans un test d'intégration, il peut être utile ou nécessaire de remplacer certains beans par des mocks. Par exemple, pour éviter d'appeler un service externe réel, pour contrôler la réponse d'un repository spécifique ou pour isoler une partie du système tout en testant le reste dans un contexte chargé.
L'annotation `@MockBean` est conçue pour cela. Lorsque vous l'utilisez sur un champ dans une classe de test annotée avec `@SpringBootTest`, Spring Boot remplacera le bean réel de ce type dans le contexte d'application par un mock Mockito. Vous pouvez ensuite configurer (`when(...)`) et vérifier (`verify(...)`) ce mock comme d'habitude.
Attention cependant : l'utilisation de `@MockBean` modifie le contexte d'application. Si un autre test utilise le même contexte mais sans ce `@MockBean`, Spring devra créer un nouveau contexte (ou recharger l'existant), ce qui peut impacter la performance globale de la suite de tests en raison de la perte du cache de contexte.
@SpringBootTest
class ServiceAvecDependanceMockeeTest {
@Autowired
private ServicePrincipal servicePrincipal;
// Remplace le bean ServiceExterne réel par un mock dans ce contexte de test
@MockBean
private ServiceExterne serviceExterneMock;
@Test
void operationPrincipale_devraitUtiliserReponseMockee() {
// Arrange: Configure le mock
when(serviceExterneMock.recupererDonneesImportantes("id123")).thenReturn("Données Mockées");
// Act
String resultat = servicePrincipal.operationComplexe("id123");
// Assert
assertThat(resultat).contains("Données Mockées");
verify(serviceExterneMock).recupererDonneesImportantes("id123");
}
// ... Supposons que ServicePrincipal dépend de ServiceExterne
}
// --- Définitions des classes (normalement dans src/main/java) ---
interface ServiceExterne { String recupererDonneesImportantes(String id); }
@Service class ServiceExterneImpl implements ServiceExterne { public String recupererDonneesImportantes(String id) { /* Appel réel */ return "Données Réelles"; } }
@Service class ServicePrincipal {
private final ServiceExterne serviceExterne;
@Autowired public ServicePrincipal(ServiceExterne se) { this.serviceExterne = se; }
public String operationComplexe(String id) { return "Résultat: " + serviceExterne.recupererDonneesImportantes(id); }
}
// Nécessite aussi @SpringBootConfiguration et @ComponentScan comme dans l'exemple précédent.
Considérations et bonnes pratiques
Bien que `@SpringBootTest` soit très puissant, son utilisation doit être réfléchie :
- Performance : Le chargement du contexte applicatif peut être long. Privilégiez les tests unitaires ou les tests de slice (`@WebMvcTest`, `@DataJpaTest`, `@RestClientTest`, etc.) lorsque c'est possible, car ils chargent un contexte plus limité et sont donc plus rapides. Réservez `@SpringBootTest` aux scénarios nécessitant réellement un contexte complet ou quasi-complet.
- Portée du test : Un test `@SpringBootTest` vérifie l'intégration de nombreux composants. S'il échoue, il peut être plus difficile d'identifier la cause racine qu'avec un test unitaire ciblé.
- Cache de contexte : Spring Test essaie de mettre en cache les contextes d'application entre les tests pour accélérer l'exécution. Cependant, si des tests ont des configurations différentes (via `@ActiveProfiles`, `@MockBean`, `@TestPropertySource`, `classes`, etc.), de nouveaux contextes seront créés, annulant les bénéfices du cache. Essayez de garder les configurations de contexte cohérentes pour les groupes de tests lorsque c'est pertinent.
- Environnement : Assurez-vous que vos tests d'intégration ne dépendent pas d'environnements externes (bases de données réelles non contrôlées, services tiers non mockés) pour garantir leur reproductibilité et leur fiabilité. Utilisez des bases de données en mémoire (H2), Testcontainers, ou des mocks (`@MockBean`) pour gérer les dépendances externes.
En résumé, `@SpringBootTest` est un outil essentiel pour valider l'intégration des composants de votre application Spring Boot dans un environnement proche de la production. Utilisez-le judicieusement en complément d'autres stratégies de test pour obtenir une couverture complète et fiable.