Skip to Content
04 TestingTests d'Intégration

Tests d’Intégration

Les tests d’intégration valident le fonctionnement complet de l’application avec toutes ses dépendances réelles (serveur HTTP, injection, etc.).

Les tests d’intégration démarrent le serveur Micronaut et testent l’application de bout en bout.


Qu’est-ce qu’un Test d’Intégration ?

Un test d’intégration vérifie que plusieurs composants fonctionnent correctement ensemble.

Caractéristiques

  • Complet - Teste toute la chaîne (Controller → UseCase → Repository)
  • Réaliste - Utilise le serveur HTTP embarqué
  • Injection réelle - Tous les beans sont injectés par Micronaut
  • Validation complète - Routing, sérialisation JSON, validation
  • Plus lent - Démarrage du serveur (2-5 secondes)

Différence avec Tests Unitaires

Test d’Intégration

@MicronautTest // Démarre le contexte complet class TrendControllerTest { @Inject @Client("/") HttpClient client; @Test void shouldReturnTrends() { // Appel HTTP réel vers le serveur var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java"), TrendResponseDto.class ); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.body().getKeyword()).isEqualTo("java"); } }

Ce qui se passe :

  • ✅ Micronaut démarre un serveur embarqué (Netty)
  • ✅ Tous les beans sont créés (Controller, UseCase, Repository)
  • ✅ HttpClient fait de vraies requêtes HTTP
  • ✅ Teste routing, JSON, validation, etc.

Annotation @MicronautTest

L’annotation @MicronautTest est la clé des tests d’intégration avec Micronaut.

Utilisation de Base

@MicronautTest class TrendControllerTest { @Inject @Client("/") HttpClient client; @Test void myTest() { // Le serveur est démarré // Tous les beans sont disponibles } }

Options Avancées

Environnement Spécifique

@MicronautTest(environments = "test") class TrendControllerTest { // Utilise application-test.yml }

application-test.yml :

micronaut: server: port: -1 # Port aléatoire pour les tests datasources: default: url: jdbc:h2:mem:testDb # Base de données en mémoire

HttpClient : Appeler l’API

Le HttpClient permet de faire des requêtes HTTP réelles vers le serveur de test.

Injection du Client

@MicronautTest class TrendControllerTest { @Inject @Client("/") // Pointe vers http://localhost:random-port HttpClient client; }

Requêtes GET

@Test void shouldGetTrends() { // Requête GET simple var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java"), TrendResponseDto.class ); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.body()).isNotNull(); }

Requêtes POST

@Test void shouldCreateTrend() { // Préparer le body CreateTrendRequest request = new CreateTrendRequest("java", "US"); // Envoyer la requête POST var response = client.toBlocking().exchange( HttpRequest.POST("/api/trends", request), TrendResponseDto.class ); assertThat(response.getStatus()).isEqualTo(HttpStatus.CREATED); assertThat(response.body()).isNotNull(); assertThat(response.body().getKeyword()).isEqualTo("java"); }

Requêtes PUT et DELETE

@Test void shouldUpdateTrend() { Long id = 1L; UpdateTrendRequest request = new UpdateTrendRequest("python"); var response = client.toBlocking().exchange( HttpRequest.PUT("/api/trends/" + id, request), TrendResponseDto.class ); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); } @Test void shouldDeleteTrend() { Long id = 1L; var response = client.toBlocking().exchange( HttpRequest.DELETE("/api/trends/" + id), Void.class ); assertThat(response.getStatus()).isEqualTo(HttpStatus.NO_CONTENT); }

Gestion des Erreurs

Les erreurs HTTP (404, 400, 500) lancent des HttpClientResponseException.

Tester les Codes d’Erreur

@Test void shouldReturn404WhenTrendNotFound() { // Tenter de récupérer un trend inexistant assertThatThrownBy(() -> client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=unknownKeyword"), TrendResponseDto.class ) ) .isInstanceOf(HttpClientResponseException.class) .satisfies(ex -> { HttpClientResponseException exception = (HttpClientResponseException) ex; assertThat(exception.getStatus()).isEqualTo(HttpStatus.NOT_FOUND); }); }

Tester les Headers HTTP

Vous pouvez vérifier et envoyer des headers personnalisés.

Envoyer des Headers

@Test void shouldAcceptAuthorizationHeader() { var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java") .header("Authorization", "Bearer token123") .header("X-Custom-Header", "value"), TrendResponseDto.class ); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); }

Vérifier les Headers de Réponse

@Test void shouldReturnCorrectContentType() { var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java"), TrendResponseDto.class ); assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.getHeaders().get("Content-Type")) .contains("application/json"); } @Test void shouldReturnCacheHeaders() { var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java"), TrendResponseDto.class ); assertThat(response.getHeaders().get("Cache-Control")) .isEqualTo("max-age=3600"); }

Exemple Complet : Test d’un Controller

Exemple complet de test d’intégration d’un controller REST.

package org.smoka.application.port.input; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.smoka.application.dto.TrendResponseDto; import static org.assertj.core.api.Assertions.*; @MicronautTest class TrendControllerTest { @Inject @Client("/") HttpClient client; @Test @DisplayName("GET /api/trends devrait retourner des tendances") void shouldReturnTrends() { // ARRANGE String keyword = "java"; // ACT var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=" + keyword), TrendResponseDto.class ); // ASSERT assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.body()).isNotNull(); assertThat(response.body().getKeyword()).isEqualTo(keyword); assertThat(response.body().getInterestScore()).isPositive(); } @Test @DisplayName("GET /api/trends devrait retourner 400 si keyword manquant") void shouldReturn400WhenKeywordMissing() { // ACT & ASSERT assertThatThrownBy(() -> client.toBlocking().exchange( HttpRequest.GET("/api/trends"), TrendResponseDto.class ) ) .isInstanceOf(HttpClientResponseException.class) .satisfies(ex -> { HttpClientResponseException exception = (HttpClientResponseException) ex; assertThat(exception.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); }); } @Test @DisplayName("GET /api/trends devrait valider le format du keyword") void shouldValidateKeywordFormat() { // Keyword avec caractères spéciaux interdits assertThatThrownBy(() -> client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=<script>alert('xss')</script>"), TrendResponseDto.class ) ) .isInstanceOf(HttpClientResponseException.class) .satisfies(ex -> { HttpClientResponseException exception = (HttpClientResponseException) ex; assertThat(exception.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST); }); } @Test @DisplayName("GET /api/trends devrait accepter une région personnalisée") void shouldAcceptCustomRegion() { // ACT var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java&region=FR"), TrendResponseDto.class ); // ASSERT assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.body().getRegion()).isEqualTo("FR"); } @Test @DisplayName("GET /api/trends devrait utiliser US comme région par défaut") void shouldUseDefaultRegion() { // ACT var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java"), TrendResponseDto.class ); // ASSERT assertThat(response.getStatus()).isEqualTo(HttpStatus.OK); assertThat(response.body().getRegion()).isEqualTo("US"); } }

Mocker des Beans dans les Tests d’Intégration

Parfois vous voulez remplacer un bean par un mock dans un test d’intégration.

Utiliser @MockBean

@MicronautTest class TrendControllerWithMockTest { @Inject @Client("/") HttpClient client; @MockBean(TrendRepository.class) TrendRepository mockRepository() { TrendRepository mock = mock(TrendRepository.class); // Définir le comportement when(mock.getTrends(any())) .thenReturn(Optional.of(new TrendResult("java", 95))); return mock; } @Test void shouldUseMockedRepository() { var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java"), TrendResponseDto.class ); // Le controller utilise le mock assertThat(response.body().getInterestScore()).isEqualTo(95); } }

Bonnes Pratiques

Suivez ces bonnes pratiques pour des tests d’intégration maintenables.

Classe de Base Commune

Évitez de dupliquer l’injection du HttpClient :

@MicronautTest public abstract class BaseIntegrationTest { @Inject @Client("/") protected HttpClient client; } // Les tests héritent class TrendControllerTest extends BaseIntegrationTest { @Test void myTest() { // client est déjà disponible var response = client.toBlocking().exchange(...); } }

Tester les Cas d’Erreur

@Test void shouldHandleNetworkTimeout() { // Simuler un timeout assertThatThrownBy(() -> client.toBlocking().exchange( HttpRequest.GET("/api/slow-endpoint"), String.class ) ) .isInstanceOf(ReadTimeoutException.class); }

Vérifier les Logs

@Test void shouldLogRequest() { // Capturer les logs (avec Logback test appender) var response = client.toBlocking().exchange( HttpRequest.GET("/api/trends?keyword=java"), TrendResponseDto.class ); // Vérifier que le log contient l'information attendue // (nécessite configuration Logback) }

Configuration des Tests

application-test.yml

Créer un fichier de configuration spécifique pour les tests :

micronaut: server: port: -1 # Port aléatoire http: client: read-timeout: 5s datasources: default: url: jdbc:h2:mem:testDb driverClassName: org.h2.Driver username: sa password: '' logger: levels: org.smoka: DEBUG

logback-test.xml

Configurer les logs pour les tests :

<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> <logger name="org.smoka" level="DEBUG" /> </configuration>

Checklist Tests d’Intégration

Pour vérifier que vos tests d’intégration sont corrects :

  • Annotation MicronautTest - Démarre le contexte
  • HttpClient injecté - Avec @Client("/")
  • Appels HTTP complets - GET, POST, PUT, DELETE
  • Gestion des erreurs - Tester 404, 400, 500
  • Validation des headers - Content-Type, Authorization
  • application-test.yml - Configuration spécifique
  • Cas limites testés - Invalid input, timeout, etc.
  • Assertions complètes - Status + Body + Headers
  • Tests isolés - Chaque test est indépendant

Résumé

Les tests d’intégration valident que l’application fonctionne correctement de bout en bout.

AspectTest Intégration
Annotation@MicronautTest
VitessePlus lent (2-5s)
ScopeToute l’application
ServeurEmbarqué (Netty)
InjectionAutomatique par Micronaut
HttpClientAppels HTTP réels
Quand utiliserEndpoints REST, flux complets

Prochaines Étapes

Maintenant que vous maîtrisez les tests d’intégration, explorez les stratégies de test.

Last updated on