Tests - JUnit 5 et Mockito
Guide complet pour ecrire des tests unitaires robustes avec JUnit 5 et Mockito: Given/When/Then, when-thenReturn, given-willReturn, verify et bonnes pratiques.
Pourquoi cette etape est importante
Un test utile ne sert pas seulement a "faire vert" dans la CI. Il sert a:
- proteger les comportements metier pendant les refactorings
- documenter les regles du systeme
- reduire le temps de debug quand une regression apparait
Un bon test unitaire est rapide, lisible, deterministe et cible une seule responsabilite.
Test unitaire: ce que tu testes vraiment
Un test unitaire valide le comportement d'une unite (souvent une classe de service) en isolation. Les dependances externes (DB, HTTP, filesystem, repository) sont remplacees par des doubles de test.
JUnit 5: fondations utiles
Annotations de base:
@Test: methode de test@BeforeEach: setup avant chaque test@AfterEach: nettoyage@DisplayName: nom lisible@Nested: regrouper des cas@ParameterizedTest: rejouer un meme test avec plusieurs entrees
Exemple minimal:
class PriceServiceTest {
private PriceService service;
@BeforeEach
void setUp() {
service = new PriceService();
}
@Test
@DisplayName("applyDiscount should reduce price for premium users")
void should_apply_discount_for_premium_user() {
int result = service.applyDiscount(100, true);
assertEquals(80, result);
}
}
Assertions courantes:
assertEquals,assertNotNull,assertTrue,assertFalseassertThrowspour verifier les exceptions
assertThrows(IllegalArgumentException.class, () -> service.applyDiscount(-1, true));
Mockito: vocabulaire essentiel
- mock: objet factice qui simule une dependance
- stub: comportement configure sur un mock
- verify: verification des interactions
- spy: enveloppe un objet reel (a utiliser avec prudence)
Exemple de setup:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
}
AAA et Given/When/Then: meme idee, deux syntaxes
Tu peux structurer un test avec:
- AAA: Arrange / Act / Assert
- BDD: Given / When / Then
Les deux racontent la meme histoire:
- Given / Arrange: contexte + stubs
- When / Act: appel de la methode testee
- Then / Assert: resultat + interactions
when(...).thenReturn(...) vs given(...).willReturn(...)
Ces deux formes font du stubbing Mockito.
Style classique:
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "briac")));
Style BDD (BDDMockito):
given(userRepository.findById(1L))
.willReturn(Optional.of(new User(1L, "briac")));
Difference principale:
- comportement identique
- style d'ecriture different
given/willReturns'aligne mieux avec la narration Given/When/Then
Exemple complet en style Given/When/Then
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void should_return_user_dto_when_user_exists() {
// Given
User user = new User(1L, "briac");
given(userRepository.findById(1L)).willReturn(Optional.of(user));
// When
UserDto result = userService.findById(1L);
// Then
assertEquals("briac", result.username());
then(userRepository).should().findById(1L);
then(userRepository).shouldHaveNoMoreInteractions();
}
}
Le role de verify / then(...).should(...)
Verifier les interactions est utile quand le comportement attendu inclut un appel collaborateur.
Style classique:
verify(userRepository).save(any(User.class));
verifyNoMoreInteractions(userRepository);
Style BDD:
then(userRepository).should().save(any(User.class));
then(userRepository).shouldHaveNoMoreInteractions();
Regle pratique: verifie les interactions seulement quand elles portent une valeur metier. Ne verifie pas tout systematiquement.
Matchers Mockito (any, eq, etc.)
Exemple:
when(userRepository.findByEmail(anyString()))
.thenReturn(Optional.empty());
Attention: si tu utilises un matcher sur un argument, utilise des matchers pour tous les arguments de cet appel.
when(client.call(eq("/users"), anyMap())).thenReturn("ok");
Tester les erreurs avec Mockito
Tu peux stubber une exception:
given(userRepository.findById(99L))
.willThrow(new RuntimeException("db unavailable"));
Puis verifier la reaction du service:
assertThrows(ServiceUnavailableException.class, () -> userService.findById(99L));
Cas ou Mockito n'est pas necessaire
N'utilise pas de mock pour les objets simples et purs (sans I/O), ex:
- utilitaires de calcul
- formatters
- mappers deterministes
Ces classes se testent mieux avec des objets reels.
Erreurs frequentes (et comment les eviter)
- Over-mocking: mocking de classes sans dependances externes
- Tests fragiles: verification de details internes non metier
- Stub inutile: configurer des retours jamais utilises
- Noms flous: tests qui ne disent pas le comportement attendu
- Given trop gros: setup trop long qui cache l'intention
Convention de nommage recommandee
Pattern simple:
should_<expected_behavior>_when_<context>
Exemples:
should_return_404_when_user_not_foundshould_apply_discount_when_customer_is_premium
Checklist qualite pour chaque test
- Le test raconte une regle metier claire
- Le setup Given est minimal
- Une seule action dans When
- Then verifie le resultat utile (et interactions seulement si necessaire)
- Le test est deterministic (pas d'horloge reseau aleatoire)
A retenir
- JUnit 5 structure les tests, Mockito isole les dependances
when/thenReturnetgiven/willReturnsont equivalents, choisis un style coherentGiven/When/Thenameliore fortement la lisibilite- Verifie le comportement metier avant les details d'implementation
- Un bon test est court, precis et explique une regle du systeme