Test en Java : des tests unitaires aux tests de bout en bout
La qualité du code est l'un des principaux indicateurs du professionnalisme d'un développeur. Dans le monde Java, les tests sont depuis longtemps devenus non seulement une bonne pratique, mais une norme de l'industrie. Sans tests fiables, il est difficile de maintenir de grands projets, d'introduire de nouvelles fonctionnalités et de refactoriser le code en toute confiance. Dans cet article, nous allons examiner trois niveaux clés de test : unit (tests unitaires), integration (tests d'intégration) et e2e (tests de bout en bout). Vous apprendrez en quoi ils diffèrent, comment les écrire correctement en Java et quels outils utiliser.
La pyramide des tests et pourquoi c'est important
Avant d'entrer dans les détails, il convient de rappeler le concept de la pyramide des tests (Mike Cohn). Elle visualise la répartition idéale des tests à différents niveaux :
- Tests unitaires (base de la pyramide) — les plus rapides, les moins chers et les plus nombreux. Ils devraient représenter environ 70%.
- Tests d'intégration (milieu) — vérifient l'interaction des composants. Ils représentent environ 20%.
- Tests E2E (sommet) — les plus lents et les plus coûteux, mais essentiels pour vérifier les scénarios utilisateur. Ils représentent environ 10%.
Le respect de cette proportion permet d'obtenir une confiance maximale dans le code avec un minimum de temps d'exécution des tests.
1. Tests unitaires en Java
Les tests unitaires vérifient de manière isolée une « unité » de code — généralement une méthode ou une classe. Leur objectif principal est de s'assurer que la logique de la méthode fonctionne correctement, indépendamment des dépendances externes (base de données, API, système de fichiers). Pour l'isolement, on utilise des objets mock.
Outils principaux
- JUnit 5 — le framework standard pour écrire des tests.
- Mockito — une bibliothèque pour créer des objets mock et vérifier les interactions.
- AssertJ — une bibliothèque pour des assertions fluides (améliore la lisibilité).
Exemple de test unitaire avec JUnit 5 et Mockito
Supposons que nous ayons un service UserService qui dépend de UserRepository :
public class UserService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } public User findById(Long id) { return repository.findById(id) .orElseThrow(() -> new UserNotFoundException("User not found")); }}Test pour cette méthode :
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 java.util.Optional;import static org.junit.jupiter.api.Assertions.*;import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)class UserServiceTest {
@Mock private UserRepository repository;
@InjectMocks private UserService userService;
@Test void testFindById_UserExists() { // Given Long userId = 1L; User expectedUser = new User(userId, "Alice"); when(repository.findById(userId)).thenReturn(Optional.of(expectedUser));
// When User actualUser = userService.findById(userId);
// Then assertNotNull(actualUser); assertEquals("Alice", actualUser.getName()); verify(repository).findById(userId); }
@Test void testFindById_UserNotFound() { // Given Long userId = 99L; when(repository.findById(userId)).thenReturn(Optional.empty());
// When & Then assertThrows(UserNotFoundException.class, () -> userService.findById(userId)); }}Bonnes pratiques :
- Testez un