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 Intégration
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 Test
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émoireHttpClient : 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
GET Simple
@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
POST avec Body
@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
404 Not Found
@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®ion=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: DEBUGlogback-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.
| Aspect | Test Intégration |
|---|---|
| Annotation | @MicronautTest |
| Vitesse | Plus lent (2-5s) |
| Scope | Toute l’application |
| Serveur | Embarqué (Netty) |
| Injection | Automatique par Micronaut |
| HttpClient | Appels HTTP réels |
| Quand utiliser | Endpoints REST, flux complets |
Prochaines Étapes
Maintenant que vous maîtrisez les tests d’intégration, explorez les stratégies de test.
- Stratégies de Test → - Quand utiliser quel type de test
- Tests Unitaires → - Tests rapides avec Mockito
- Tests Manuels → - Tester avec cURL et Postman