Introduction : Pourquoi les tests sont le fondement d'un code Java de qualité
Chaque développeur Java a déjà été confronté à une situation où, après avoir corrigé un bug, trois nouveaux apparaissaient. Pour briser ce cercle vicieux, il existe les tests. Ce n'est pas simplement une case à cocher dans une liste, mais une discipline professionnelle qui préserve les nerfs, l'argent et la réputation du produit.
Dans cet article, nous allons décomposer les trois niveaux clés de test : unit (tests unitaires), integration (tests d'intégration) et e2e (tests de bout en bout). Nous les examinerons à travers des exemples pratiques en Java, en utilisant des frameworks populaires : JUnit 5, Mockito, Spring Boot Test et Selenium. Vous apprendrez à construire une pyramide de tests correcte et à ne pas surcharger votre projet avec des tests inutiles.
1. Tests unitaires : Vérification des unités atomiques de code
Les tests unitaires sont le niveau le plus bas et le plus rapide. Ils vérifient le fonctionnement d'une seule « unité » (généralement une méthode ou une classe) en isolation des dépendances externes (BDD, API, système de fichiers).
1.1 Outils et frameworks
- JUnit 5 — le standard de facto pour écrire des tests en Java.
- Mockito — une bibliothèque pour créer des bouchons (mocks) et des stubs.
- AssertJ — une bibliothèque pour des assertions fluides.
1.2 Exemple de test unitaire avec JUnit 5 et Mockito
Imaginons que nous ayons un service OrderService qui dépend d'un repository PaymentRepository. Nous voulons tester la logique du service sans appeler la vraie base de données.
// Classe à tester
public class OrderService {
private final PaymentRepository paymentRepository;
public OrderService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
public String processOrder(Order order) {
if (order.getAmount() <= 0) {
throw new IllegalArgumentException("Le montant de la commande doit être positif");
}
boolean success = paymentRepository.pay(order);
return success ? "Paiement réussi" : "Erreur de paiement";
}
}
// Test unitaire
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private PaymentRepository paymentRepository;
@InjectMocks
private OrderService orderService;
@Test
void testProcessOrder_Success() {
// Arrange
Order order = new Order(100.0);
when(paymentRepository.pay(order)).thenReturn(true);
// Act
String result = orderService.processOrder(order);
// Assert
assertEquals("Paiement réussi", result);
verify(paymentRepository, times(1)).pay(order);
}
@Test
void testProcessOrder_NegativeAmount() {
// Arrange
Order order = new Order(-10.0);
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> {
orderService.processOrder(order);
});
}
}
Bonnes pratiques pour les tests unitaires :
- Un test = une vérification (principe FIRST : Fast, Isolated, Repeatable, Self-validating, Timely).
- Utilisez des noms explicites :
shouldReturnSuccess_whenPaymentIsApproved. - Ne testez pas les méthodes privées — testez le contrat public.
- Couvrez les cas limites : null, chaînes vides, nombres négatifs.
2. Tests d'intégration : Vérification de l'interaction des composants
Les tests d'intégration vérifient comment différents modules du système
```