Tests en Java : Unitaires, d'Intégration, E2E — Niveau Avancé
Dans le développement Java moderne, les tests ne sont pas une simple formalité, mais une pratique fondamentale garantissant la stabilité et la prévisibilité du code. Les tests de base, comme les simples @Test avec Assert.assertEquals, ont depuis longtemps cédé la place à des stratégies multicouches complexes. Dans cet article, nous analyserons les techniques avancées pour chaque niveau de la pyramide de test : Unitaires, d'Intégration et End-to-End (E2E). Vous apprendrez à écrire des tests maintenables, rapides et fiables qui transformeront votre code en un mécanisme parfaitement rodé.
1. Tests Unitaires Avancés : Mocks, Stubs et Paramétrage
Les tests unitaires vérifient une partie isolée du code (généralement une classe ou une méthode). Au niveau avancé, nous passons de scénarios simples à des interactions complexes et des cas limites. L'outil clé ici est la bibliothèque Mockito.
1.1. Utilisation d'ArgumentCaptor et de réponses complexes (Answer)
Il est souvent nécessaire non seulement de vérifier qu'une méthode a été appelée, mais aussi quels arguments lui ont été passés. Pour cela, on utilise ArgumentCaptor.
// Exemple : Vérification que le bon message a été envoyé au service@ExtendWith(MockitoExtension.class)public class NotificationServiceTest { @Mock private EmailSender emailSender; @InjectMocks private NotificationService notificationService;
@Test void shouldSendEmailWithCorrectContent() { // Arrange User user = new User("test@example.com", "Test User"); // Act notificationService.sendWelcomeEmail(user); // Assert ArgumentCaptor<EmailMessage> captor = ArgumentCaptor.forClass(EmailMessage.class); verify(emailSender).send(captor.capture()); EmailMessage capturedMessage = captor.getValue(); assertEquals("test@example.com", capturedMessage.getTo()); assertTrue(capturedMessage.getBody().contains("Test User")); }}Pour créer dynamiquement des réponses basées sur les données d'entrée, utilisez l'interface Answer :
@Testvoid testDynamicResponse() { when(dataRepository.findById(anyLong())).thenAnswer(invocation -> { Long id = invocation.getArgument(0); return new DataEntity(id, "Entity with ID: " + id); }); assertEquals("Entity with ID: 5", dataService.getDataById(5L).getName());}1.2. Tests Paramétrés (JUnit 5)
Le paramétrage permet d'exécuter un même test avec différents ensembles de données, évitant ainsi la duplication de code. JUnit 5 fournit des annotations puissantes comme @CsvSource et @MethodSource.
@ParameterizedTest@CsvSource({ "10, 20, 30", "0, 0, 0", "-5, 5, 0"})void testSum(int a, int b, int expectedSum) { Calculator calculator = new Calculator(); assertEquals(expectedSum, calculator.add(a, b));}2. Tests d'Intégration : Travail avec la base de données et les services externes
Les tests d'intégration vérifient l'interaction entre les composants du système, par exemple avec une base de données réelle ou des files d'attente. Le principal défi est la gestion de l'état de l'environnement. C'est là que Testcontainers vient à la rescousse.
2.1. Testcontainers pour une base de données isolée
Testcontainers lance des conteneurs Docker avec le logiciel nécessaire (PostgreSQL, Redis, Kafka) directement pendant les tests, garantissant la propreté et la reproductibilité.
@SpringBootTest@Testcontainersclass UserRepositoryIntegrationTest {
@Container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15") .withDatabaseName("testdb") .withUsername("test") .withPassword("test");
@DynamicPropertySource static void configureProperties(DynamicPropertyR