🎯 OBJECTIF
Comprendre comment :
@Captorverify() et ses varianteseq, any, argThat) selon le besoindoReturn est nécessaire sur un spyassertThrows, thenThrow, doThrow🧠 MODÈLE MENTAL
Un test unitaire teste une seule classe isolée, sans dépendances réelles — pas de DB, pas d'Hibernate, pas de Spring, pas de réseau. Il doit être rapide (millisecondes), déterministe, et isolé.
Mockito crée un proxy dynamique qui intercepte tous les appels vers les dépendances, retourne ce que tu programmes, et enregistre chaque interaction. verify() exploite cet historique pour confirmer qu'un appel a bien eu lieu. ArgumentCaptor te permet d'inspecter l'argument réel qui a été passé — utile quand l'objet est construit dans le code testé et contient des champs dynamiques (UUID, timestamp).
La règle clé : when() évalue l'appel pendant la configuration du stub — sur un spy ou un objet réel, la vraie méthode peut s'exécuter à ce moment-là. doReturn() n'appelle jamais la vraie méthode. Sur un mock pur, les deux sont équivalents. Sur un spy, toujours doReturn.
// STUB — retourne une valeur programmée
when(repo.findById("S1")).thenReturn(Optional.of(stock));
// MOCK — stub + vérification d'appel
verify(repo).findById("S1");
// FAKE — implémentation simplifiée réelle (sans Mockito)
class FakeRepo implements StockRepository {
Map<String, Stock> map = new HashMap<>();
public Optional<Stock> findById(String id) {
return Optional.ofNullable(map.get(id));
}
}
// SPY — enveloppe un vrai objet
List<String> spyList = spy(new ArrayList<>());
spyList.add("A");
verify(spyList).add("A"); // enregistré ✓
assertEquals(1, spyList.size()); // vraie méthode exécutée ✓java| Mock | Spy | Fake | |
|---|---|---|---|
| Vraie logique | ❌ | ✅ (par défaut) | ✅ |
| Vérification | ✅ | ✅ | ❌ |
| Stubbing | ✅ | ✅ (partiel) | N/A |
| Usage | Remplacer une dépendance | Observer/modifier un objet réel | Simplifier une implémentation |
@ExtendWith(MockitoExtension.class) // ✓ active Mockito pour JUnit 5
class StockServiceTest {
@Mock
StockRepository repo; // ✓ équivalent mock(StockRepository.class)
@InjectMocks
StockService service; // ✓ équivalent new StockService(repo)
@Captor
ArgumentCaptor<Stock> captor;
}java🚨 @InjectMocks crée l'objet — il ne l'injecte pas dans Spring
Aucun contexte Spring dans un test unitaire. C'est intentionnel — c'est ce qui rend le test rapide et isolé. Si tu as besoin du contexte Spring, c'est un test d'intégration.
Utile quand l'objet passé à un mock est construit dans le code testé et contient des valeurs dynamiques (UUID, timestamp, champs calculés).
// Code testé
public void save(String id) {
Stock stock = new Stock(id, UUID.randomUUID(), Instant.now());
repo.save(stock);
}
// Test
@Test
void shouldSaveWithCorrectId() {
service.save("S1");
verify(repo).save(captor.capture()); // ✓ capture l'argument réel
Stock saved = captor.getValue();
assertThat(saved.getId()).isEqualTo("S1");
assertNotNull(saved.getCreatedAt()); // ✓ inspecte les champs dynamiques
}javaverify(repo).save(stock); // appelé exactement 1 fois (défaut)
verify(repo, times(3)).save(stock); // exactement 3 fois
verify(repo, never()).save(stock); // jamais appelé
verify(repo, atLeast(1)).save(stock); // >= 1 fois
verifyNoInteractions(repo); // aucun appel sur ce mock
verifyNoMoreInteractions(repo); // aucun appel non encore vérifiéjava// Valeur directe — quand la valeur exacte est connue
verify(repo).findById("S1");
// eq() — pour mixer avec d'autres matchers
verify(repo).find(eq("P1"), anyInt());
// any() / anyString() / anyInt()
verify(repo).save(any(Stock.class));
// argThat() — condition métier
verify(repo).save(argThat(s -> s.getQuantity().compareTo(BigDecimal.ZERO) > 0));java🚨 Règle critique des matchers
Si tu utilises un matcher, tous les arguments doivent être des matchers.
// ❌ plante à l'exécution (InvalidUseOfMatchersException)
verify(repo).find("P1", anyInt());
// ✓ correct
verify(repo).find(eq("P1"), anyInt());java// ✓ Sur un MOCK pur — when() est safe
@Mock StockRepository repo;
when(repo.findById("S1")).thenReturn(Optional.of(stock));
// ✓ Sur un SPY — doReturn() OBLIGATOIRE
StockService spy = spy(new StockService(repo));
doReturn(BigDecimal.TEN)
.when(spy)
.computeQuantity(any()); // ✓ la vraie méthode n'est PAS appeléejava🚨 when() sur un spy peut déclencher la vraie méthode
// ❌ DANGEREUX — computeQuantity() s'exécute pendant le stubbing
when(spy.computeQuantity(any())).thenReturn(BigDecimal.TEN);
// Si elle accède à la DB ou throw → test explose avant la première assertionjava// assertThrows — tester qu'une exception est lancée
when(repo.findById("S1")).thenReturn(Optional.empty());
RuntimeException ex = assertThrows(
RuntimeException.class,
() -> service.find("S1")
);
assertEquals("Stock not found", ex.getMessage());
verify(repo, never()).delete(any()); // ✓ vérifier qu'aucune suite n'a eu lieu
// thenThrow — méthode avec retour
when(repo.findById("S1")).thenThrow(new RuntimeException("DB error"));
// doThrow — méthode void
doThrow(new RuntimeException("DB error")).when(repo).save(any());
// Comportements enchaînés
when(repo.findById("S1"))
.thenThrow(new RuntimeException())
.thenReturn(Optional.of(stock)); // 1er appel → exception, 2ème → stockjava⚡ TL;DR — chaque concept en une ligne
Mock
✓ Objet fake contrôlé par Mockito — intercepte les appels, retourne ce que tu programmes, enregistre pour verify().
⚠ Aucune vraie logique n'est exécutée.
Spy ✓ Enveloppe un objet réel — exécute la vraie logique par défaut, permet de stubber certaines méthodes. ⚠ Ne pas utiliser pour remplacer une dépendance (c'est le rôle du mock) — l'utiliser pour observer ou modifier partiellement un objet réel.
@Captor / ArgumentCaptor
✓ Capture l'argument réel passé à un mock — indispensable quand l'objet contient des valeurs dynamiques (UUID, timestamp).
⚠ Inutile si l'objet est prévisible — verify(repo).save(expectedStock) suffit.
when() vs doReturn()
✓ when() sur mock pur — aucun risque. doReturn() sur spy — n'exécute jamais la vraie méthode.
⚠ when(spy.method()) évalue la méthode pendant le stubbing — si elle throw ou accède à la DB, le test explose avant la première assertion.
Matchers
✓ any() quand la valeur n'a pas d'importance, eq() pour mixer avec une valeur exacte, argThat() pour une condition métier.
⚠ Mélanger valeur directe et matcher compile mais lève InvalidUseOfMatchersException à l'exécution.
🎓 À retenir
@InjectMocks ne démarre pas Spring — il instancie la classe avec les mocks. Tout contexte Spring dans un test unitaire est un symptôme d'un test mal isolé.verify() ne remplace pas une assertion — vérifier qu'un appel a eu lieu ne dit rien sur le résultat. Combiner verify() (a-t-on appelé ?) avec assertThat() (le résultat est-il correct ?).captor.getAllValues() vs captor.getValue() — si la méthode est appelée plusieurs fois, getAllValues() retourne tous les arguments capturés. getValue() retourne seulement le dernier.