Chapitre 4: Tests d'Intégration
Introduction
Les tests unitaires avec mocks sont rapides mais ne garantissent pas que les composants fonctionnent ensemble. Les tests d'intégration vérifient que Spring, JPA, et votre base de données collaborent correctement.
🎯 Objectifs du Chapitre
À la fin de ce chapitre, vous saurez:
- ✅ Utiliser @SpringBootTest
- ✅ Tester les repositories avec @DataJpaTest
- ✅ Tester les controllers avec @WebMvcTest
- ✅ Configurer les profils de test
- ✅ Utiliser TestContainers (bonus)
1. @SpringBootTest
Test d'Intégration Complet
@SpringBootTest // ← Charge le contexte Spring complet
class ApplicationIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@Test
void contextLoads() {
// Vérifie que l'application démarre
assertNotNull(userService);
}
@Test
void shouldCreateAndFindUser() {
User user = userService.create("John", "john@test.com");
User found = userService.findById(user.getId());
assertEquals("John", found.getName());
}
}
Options de @SpringBootTest
// Contexte complet avec serveur web
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FullStackTest {
@LocalServerPort
private int port;
}
// Sans serveur web (plus rapide)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class ServiceTest { }
// Avec configuration spécifique
@SpringBootTest(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"logging.level.root=WARN"
})
class ConfiguredTest { }
2. @DataJpaTest
Test de Repository
@DataJpaTest // ← Configure uniquement JPA + H2
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
// Arrange
User user = new User("John", "john@test.com");
entityManager.persistAndFlush(user);
// Act
Optional<User> found = userRepository.findByEmail("john@test.com");
// Assert
assertTrue(found.isPresent());
assertEquals("John", found.get().getName());
}
@Test
void shouldReturnEmptyWhenEmailNotFound() {
Optional<User> found = userRepository.findByEmail("unknown@test.com");
assertFalse(found.isPresent());
}
}
Ce que @DataJpaTest Configure
@DataJpaTest inclut:
├── H2 en mémoire (par défaut)
├── Hibernate/JPA
├── TestEntityManager
├── Repositories
└── @Transactional (rollback après chaque test)
N'inclut PAS:
├── @Service
├── @Controller
├── Spring Security
└── Autres composants
Tester les Requêtes Personnalisées
@DataJpaTest
class ProductRepositoryTest {
@Autowired
private ProductRepository productRepository;
@Autowired
private TestEntityManager em;
@BeforeEach
void setUp() {
em.persist(new Product("Laptop", 1000.0, "Electronics"));
em.persist(new Product("Phone", 800.0, "Electronics"));
em.persist(new Product("Shirt", 50.0, "Clothing"));
em.flush();
}
@Test
void shouldFindByCategory() {
List<Product> electronics = productRepository.findByCategory("Electronics");
assertThat(electronics).hasSize(2);
}
@Test
void shouldFindByPriceRange() {
List<Product> products = productRepository.findByPriceBetween(100.0, 900.0);
assertThat(products)
.hasSize(1)
.extracting(Product::getName)
.containsExactly("Phone");
}
@Test
void shouldCountByCategory() {
long count = productRepository.countByCategory("Electronics");
assertEquals(2, count);
}
}
3. @WebMvcTest
Test de Controller (sans service réel)
@WebMvcTest(UserController.class) // ← Teste uniquement ce controller
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // ← Mock le service
private UserService userService;
@Test
void shouldReturnUser() throws Exception {
// Arrange
when(userService.findById(1L))
.thenReturn(new User(1L, "John", "john@test.com"));
// Act & Assert
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("john@test.com"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(anyLong()))
.thenThrow(new UserNotFoundException(999L));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound());
}
}
@MockBean vs @Mock
// @MockBean: Pour les tests Spring (injecté dans le contexte)
@WebMvcTest
class ControllerTest {
@MockBean
private UserService userService; // Remplace le bean Spring
}
// @Mock: Pour les tests unitaires purs (Mockito seul)
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock
private UserRepository userRepository; // Mock Mockito simple
}
4. Profils de Test
application-test.properties
# src/test/resources/application-test.properties
# Base de données H2
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
# Désactiver des features
spring.flyway.enabled=false
spring.mail.host=localhost
Activer le Profil
@SpringBootTest
@ActiveProfiles("test") // ← Utilise application-test.properties
class MyIntegrationTest {
// ...
}
Annotation @TestPropertySource
@SpringBootTest
@TestPropertySource(properties = {
"spring.jpa.show-sql=false",
"app.feature.enabled=true"
})
class OverriddenPropertiesTest {
// ...
}
5. @Transactional dans les Tests
Rollback Automatique
@SpringBootTest
@Transactional // ← Rollback après chaque test
class TransactionalTest {
@Autowired
private UserRepository userRepository;
@Test
void test1() {
userRepository.save(new User("John"));
assertEquals(1, userRepository.count());
}
@Test
void test2() {
// La BDD est vide car test1 a été rollback
assertEquals(0, userRepository.count());
}
}
@DataJpaTest est @Transactional par Défaut
@DataJpaTest // ← Déjà @Transactional
class RepositoryTest {
// Rollback automatique après chaque test
}
6. Test avec Données Initiales
@Sql
@SpringBootTest
@Sql("/test-data.sql") // ← Exécute avant chaque test
class SqlDataTest {
// ...
}
@Test
@Sql(scripts = "/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void testWithCleanup() {
// ...
}
@SqlGroup
@SpringBootTest
@SqlGroup({
@Sql("/schema.sql"),
@Sql("/test-data.sql")
})
class MultiSqlTest {
// ...
}
Fichier test-data.sql
-- src/test/resources/test-data.sql
INSERT INTO users (id, name, email) VALUES (1, 'John', 'john@test.com');
INSERT INTO users (id, name, email) VALUES (2, 'Jane', 'jane@test.com');
INSERT INTO products (id, name, price) VALUES (1, 'Laptop', 1000.00);
7. TestContainers (Bonus)
Pourquoi?
H2 est pratique mais différent de PostgreSQL/MySQL. TestContainers lance de vraies bases de données dans Docker.
Configuration
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
Utilisation
@SpringBootTest
@Testcontainers
class PostgreSQLIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveToRealPostgres() {
User user = userRepository.save(new User("John", "john@test.com"));
assertNotNull(user.getId());
}
}
8. Slices de Test Spring Boot
Récapitulatif
| Annotation | Contexte Chargé | Utilisation |
|---|---|---|
@SpringBootTest | Tout | Tests d'intégration complets |
@DataJpaTest | JPA + H2 | Tests de repository |
@WebMvcTest | MVC + Security | Tests de controller |
@JsonTest | Jackson | Tests de sérialisation |
@RestClientTest | RestTemplate | Tests de clients REST |
Combiner avec @MockBean
@DataJpaTest
@Import(UserService.class) // Importer le service
class UserServiceWithRealDbTest {
@Autowired
private UserService userService;
@MockBean
private EmailService emailService; // Mock externe
@Test
void shouldSaveUser() {
// userService utilise le vrai repository
// emailService est mocké
}
}
9. Résumé
Annotations Clés
| Annotation | Usage |
|---|---|
@SpringBootTest | Contexte complet |
@DataJpaTest | Tests JPA/Repository |
@WebMvcTest | Tests Controller |
@MockBean | Mock dans le contexte Spring |
@ActiveProfiles | Profil de test |
@Sql | Données SQL |
@Transactional | Rollback automatique |
Choix du Type de Test
┌─────────────────────────────────────────────┐
│ Que tester? │
├──────────────────────┬──────────────────────┤
│ Repository │ @DataJpaTest │
│ Controller seul │ @WebMvcTest │
│ Service avec mocks │ @ExtendWith(Mockito) │
│ Stack complète │ @SpringBootTest │
│ Avec vraie BDD │ TestContainers │
└──────────────────────┴──────────────────────┘
🎯 Prochaine Étape
Vous savez tester les repositories et services. Maintenant, apprenons à tester l'API REST en détail avec MockMvc.