Aller au contenu principal

Chapitre 3: Mocking avec Mockito

Introduction​

Comment tester un UserService qui dépend d'un UserRepository? Sans la base de données, le test échoue. La solution: Mocker le repository pour simuler son comportement.


🎯 Objectifs du Chapitre​

À la fin de ce chapitre, vous saurez:

  1. ✅ Comprendre pourquoi et quand mocker
  2. ✅ Créer des mocks avec @Mock et @InjectMocks
  3. ✅ Configurer le comportement avec when().thenReturn()
  4. ✅ Vérifier les appels avec verify()
  5. ✅ Utiliser les argument matchers

1. Pourquoi Mocker?​

Le Problème​

// Cette classe dépend de UserRepository
@Service
public class UserService {
private final UserRepository userRepository;

public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}

// Comment tester sans base de données?
@Test
void shouldFindUserById() {
UserService service = new UserService(???); // Il faut un repository!
}

La Solution: Mocking​

@Test
void shouldFindUserById() {
// Créer un FAUX repository
UserRepository mockRepo = mock(UserRepository.class);

// Lui dire quoi retourner
when(mockRepo.findById(1L)).thenReturn(Optional.of(new User("John")));

// Tester le service
UserService service = new UserService(mockRepo);
User user = service.findById(1L);

assertEquals("John", user.getName());
}

2. Configuration de Base​

Avec Annotations (Recommandé)​

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;

@ExtendWith(MockitoExtension.class) // ← Active Mockito
class UserServiceTest {

@Mock // ← Crée un mock
private UserRepository userRepository;

@InjectMocks // ← Injecte les mocks
private UserService userService;

@Test
void shouldFindUserById() {
// when/then ici
}
}

Flux d'Injection​

@Mock userRepository ─────┐
├──→ @InjectMocks userService
@Mock anotherDep ─────────┘

3. when().thenReturn()​

Retourner une Valeur​

@Test
void shouldReturnUser() {
// Configurer le mock
User mockUser = new User(1L, "John", "john@email.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));

// Tester
User result = userService.findById(1L);

// Vérifier
assertEquals("John", result.getName());
}

Retourner Différentes Valeurs​

@Test
void shouldReturnDifferentUsers() {
when(userRepository.findById(1L)).thenReturn(Optional.of(new User("John")));
when(userRepository.findById(2L)).thenReturn(Optional.of(new User("Jane")));
when(userRepository.findById(99L)).thenReturn(Optional.empty());

assertEquals("John", userService.findById(1L).getName());
assertEquals("Jane", userService.findById(2L).getName());
assertThrows(UserNotFoundException.class, () -> userService.findById(99L));
}

Appels Consécutifs​

@Test
void shouldReturnDifferentValuesOnConsecutiveCalls() {
when(userRepository.count())
.thenReturn(0L)
.thenReturn(1L)
.thenReturn(2L);

assertEquals(0L, userRepository.count()); // 1er appel
assertEquals(1L, userRepository.count()); // 2ème appel
assertEquals(2L, userRepository.count()); // 3ème appel
assertEquals(2L, userRepository.count()); // Suite: dernière valeur
}

4. thenThrow() - Simuler des Exceptions​

@Test
void shouldThrowException() {
when(userRepository.findById(anyLong()))
.thenThrow(new DatabaseException("Connection failed"));

assertThrows(DatabaseException.class, () -> userService.findById(1L));
}

@Test
void shouldThrowThenSucceed() {
when(userRepository.save(any()))
.thenThrow(new RuntimeException("First call fails"))
.thenReturn(new User("Saved"));

// 1er appel: exception
assertThrows(RuntimeException.class, () -> userService.save(new User()));

// 2ème appel: succès
User saved = userService.save(new User());
assertNotNull(saved);
}

5. Argument Matchers​

any(), anyLong(), anyString()​

@Test
void shouldAcceptAnyId() {
when(userRepository.findById(anyLong())) // N'importe quel Long
.thenReturn(Optional.of(new User("Generic")));

assertEquals("Generic", userService.findById(1L).getName());
assertEquals("Generic", userService.findById(999L).getName());
}

Matchers Disponibles​

any()           // N'importe quel objet
any(User.class) // N'importe quel User
anyLong() // N'importe quel long
anyInt() // N'importe quel int
anyString() // N'importe quelle String
anyList() // N'importe quelle List
anyMap() // N'importe quelle Map
eq(value) // Égalité exacte (avec autres matchers)
isNull() // Null
isNotNull() // Non null

Règle Importante: Tout ou Rien​

// ❌ ERREUR: Mélange matcher et valeur exacte
when(service.process(anyString(), "fixed")) // ERREUR!

// ✅ OK: Utiliser eq() pour la valeur exacte
when(service.process(anyString(), eq("fixed")))

argThat() - Matcher Personnalisé​

@Test
void shouldMatchCustomCondition() {
when(userRepository.save(argThat(user ->
user.getName() != null && user.getName().length() > 3
))).thenReturn(new User("Saved"));

// Fonctionne
User saved = userService.save(new User("John"));
assertNotNull(saved);

// Ne match pas -> retourne null (comportement par défaut du mock)
User notSaved = userService.save(new User("Jo"));
assertNull(notSaved);
}

6. verify() - Vérifier les Appels​

Vérification Simple​

@Test
void shouldCallRepository() {
userService.delete(1L);

// Vérifie que deleteById a été appelé avec 1L
verify(userRepository).deleteById(1L);
}

Nombre d'Appels​

@Test
void shouldCallRepositoryCorrectTimes() {
userService.processAll();

verify(userRepository, times(3)).save(any()); // Exactement 3 fois
verify(userRepository, atLeast(2)).findById(any()); // Au moins 2 fois
verify(userRepository, atMost(5)).count(); // Au plus 5 fois
verify(userRepository, never()).delete(any()); // Jamais appelé
}

Ordre des Appels​

@Test
void shouldCallInOrder() {
userService.createAndNotify(new User("John"));

InOrder inOrder = inOrder(userRepository, emailService);

// Vérifie l'ordre
inOrder.verify(userRepository).save(any()); // D'abord save
inOrder.verify(emailService).sendWelcome(any()); // Puis email
}

verifyNoMoreInteractions​

@Test
void shouldOnlyCallSpecificMethods() {
userService.findById(1L);

verify(userRepository).findById(1L);
verifyNoMoreInteractions(userRepository); // Aucun autre appel
}

7. @Spy - Mock Partiel​

Différence Mock vs Spy​

// Mock: Tous les appels sont simulés (retournent null/0/false par défaut)
@Mock
private UserService mockService;

// Spy: Vraie implémentation, mais on peut override certaines méthodes
@Spy
private UserService spyService;

Utilisation​

@ExtendWith(MockitoExtension.class)
class PartialMockTest {

@Spy
private ArrayList<String> spyList = new ArrayList<>();

@Test
void testSpy() {
// Appel réel
spyList.add("one");
spyList.add("two");
assertEquals(2, spyList.size());

// Override le comportement
when(spyList.size()).thenReturn(100);
assertEquals(100, spyList.size());

// L'élément est vraiment là
assertEquals("one", spyList.get(0));
}
}

doReturn() vs when() avec Spy​

@Spy
private UserService userService;

@Test
void testSpy() {
// ❌ PROBLÈME: when() appelle la vraie méthode d'abord!
when(userService.riskyMethod()).thenReturn("safe");

// ✅ SOLUTION: doReturn() n'appelle pas la vraie méthode
doReturn("safe").when(userService).riskyMethod();
}

8. Capturer les Arguments​

ArgumentCaptor​

@Test
void shouldCaptureArgument() {
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);

userService.register("John", "john@email.com", "password");

verify(userRepository).save(userCaptor.capture());

User savedUser = userCaptor.getValue();
assertEquals("John", savedUser.getName());
assertEquals("john@email.com", savedUser.getEmail());
assertNotNull(savedUser.getPassword()); // Encodé
}

Avec Annotation​

@Captor
private ArgumentCaptor<User> userCaptor;

@Test
void shouldCaptureMultipleArgs() {
userService.saveAll(List.of(new User("A"), new User("B")));

verify(userRepository, times(2)).save(userCaptor.capture());

List<User> capturedUsers = userCaptor.getAllValues();
assertEquals(2, capturedUsers.size());
assertEquals("A", capturedUsers.get(0).getName());
assertEquals("B", capturedUsers.get(1).getName());
}

9. Exemple Complet​

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

@Mock
private OrderRepository orderRepository;

@Mock
private ProductService productService;

@Mock
private EmailService emailService;

@InjectMocks
private OrderService orderService;

@Captor
private ArgumentCaptor<Order> orderCaptor;

@Test
void shouldCreateOrderSuccessfully() {
// Arrange
Product product = new Product(1L, "Laptop", 1000.0);
when(productService.findById(1L)).thenReturn(Optional.of(product));
when(orderRepository.save(any())).thenAnswer(inv -> {
Order order = inv.getArgument(0);
order.setId(100L);
return order;
});

// Act
Order order = orderService.createOrder(1L, 2, "john@email.com");

// Assert
assertNotNull(order);
assertEquals(100L, order.getId());
assertEquals(2000.0, order.getTotal());

// Verify
verify(productService).findById(1L);
verify(orderRepository).save(orderCaptor.capture());
verify(emailService).sendOrderConfirmation(eq("john@email.com"), any());

Order savedOrder = orderCaptor.getValue();
assertEquals(2, savedOrder.getQuantity());
}

@Test
void shouldThrowWhenProductNotFound() {
// Arrange
when(productService.findById(anyLong())).thenReturn(Optional.empty());

// Act & Assert
assertThrows(ProductNotFoundException.class,
() -> orderService.createOrder(999L, 1, "test@email.com"));

// Verify no order was saved
verify(orderRepository, never()).save(any());
verify(emailService, never()).sendOrderConfirmation(anyString(), any());
}
}

10. Résumé​

Annotations​

AnnotationDescription
@MockCrée un mock
@InjectMocksInjecte les mocks dans la classe testée
@SpyMock partiel (vraie implémentation)
@CaptorCapture les arguments
@ExtendWith(MockitoExtension.class)Active Mockito

Méthodes Clés​

// Configuration
when(mock.method()).thenReturn(value);
when(mock.method()).thenThrow(exception);
doReturn(value).when(spy).method();

// Vérification
verify(mock).method();
verify(mock, times(3)).method();
verify(mock, never()).method();
verifyNoMoreInteractions(mock);

// Matchers
any(), anyLong(), anyString()
eq(value)
argThat(condition)

🎯 Prochaine Étape​

Mockito est parfait pour les tests unitaires, mais parfois vous avez besoin de tester avec de vraies ressources. C'est le rôle des tests d'intégration.

➡️ Chapitre 4: Tests d'Intégration


⬅️ Retour au Module 9