
Utilisation de Mockito pour mocker les dépendances (`@MockBean`)
Apprenez à isoler vos composants pour les tests unitaires et d'intégration dans Spring Boot en utilisant Mockito et l'annotation @MockBean.
Pourquoi mocker les dépendances ?
Lors de l'écriture de tests, en particulier les tests unitaires et certains tests d'intégration, il est crucial d'isoler le composant que l'on souhaite tester (le "System Under Test" ou SUT). Les dépendances externes (autres services, repositories, clients API, etc.) peuvent introduire de la complexité, de l'instabilité (par exemple, nécessité d'une base de données active) ou des effets de bord indésirables dans nos tests.
Le "mocking" (ou création de bouchons) consiste à remplacer ces dépendances réelles par des objets factices, appelés "mocks". Ces mocks simulent le comportement des dépendances réelles de manière contrôlée. Cela permet de se concentrer exclusivement sur la logique du SUT, de définir précisément comment les dépendances doivent réagir pendant le test, et de vérifier que le SUT interagit correctement avec elles.
Mockito est la bibliothèque de mocking la plus populaire dans l'écosystème Java. Spring Boot s'intègre parfaitement avec Mockito, notamment grâce à l'annotation `@MockBean`, qui facilite grandement la création et l'injection de mocks dans le contexte de test Spring.
Introduction à `@MockBean`
L'annotation `@MockBean`, fournie par le module `spring-boot-test`, est un outil puissant pour intégrer Mockito au contexte d'application Spring lors des tests. Lorsque vous annotez un champ ou une méthode de configuration avec `@MockBean`, Spring va remplacer tout bean existant du même type dans le contexte d'application par un mock Mockito. S'il n'existe pas de bean de ce type, un nouveau mock sera ajouté au contexte.
La principale force de `@MockBean` réside dans sa capacité à injecter ce mock partout où la dépendance réelle aurait été injectée (via `@Autowired`, injection par constructeur, etc.) dans le contexte de test. Cela simplifie considérablement la configuration des tests d'intégration où vous avez besoin de charger une partie du contexte Spring tout en bouchonnant certaines dépendances spécifiques.
Il est important de distinguer `@MockBean` de l'annotation `@Mock` de Mockito. `@Mock` crée un mock standard qui doit être géré manuellement (par exemple, avec `MockitoAnnotations.openMocks(this)`), tandis que `@MockBean` gère l'intégration du mock directement dans le contexte Spring et son cycle de vie.
Utilisation pratique de `@MockBean` dans les tests
Voyons un exemple concret. Supposons un `OrderService` qui dépend d'un `OrderRepository` pour sauvegarder les commandes.
// Interface du Repository
public interface OrderRepository extends JpaRepository {
// Méthodes spécifiques si nécessaire
}
// Service métier
@Service
public class OrderService {
private final OrderRepository orderRepository;
@Autowired
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public Order placeOrder(Order order) {
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
// Logique métier complexe...
return orderRepository.save(order);
}
}
Pour tester `OrderService` en isolant la base de données, nous pouvons utiliser `@MockBean` pour le `OrderRepository` dans un test d'intégration `@SpringBootTest` :
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.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.Collections;
@SpringBootTest // Charge le contexte Spring
class OrderServiceIntegrationTest {
@Autowired // Le service réel est injecté depuis le contexte
private OrderService orderService;
@MockBean // Remplace/ajoute un mock du repository dans le contexte
private OrderRepository orderRepositoryMock;
@Test
void placeOrder_shouldSaveOrder_whenOrderIsValid() {
// Arrange
Order newOrder = new Order();
newOrder.setItems(Collections.singletonList("Item1"));
Order savedOrder = new Order();
savedOrder.setId(1L);
savedOrder.setItems(Collections.singletonList("Item1"));
// Définir le comportement du mock
when(orderRepositoryMock.save(any(Order.class))).thenReturn(savedOrder);
// Act
Order result = orderService.placeOrder(newOrder);
// Assert
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
// Vérifier que la méthode save a été appelée sur le mock
verify(orderRepositoryMock, times(1)).save(newOrder);
}
@Test
void placeOrder_shouldThrowException_whenOrderHasNoItems() {
// Arrange
Order emptyOrder = new Order();
// Act & Assert
assertThatThrownBy(() -> orderService.placeOrder(emptyOrder))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Order must have items");
// Vérifier que la méthode save n'a JAMAIS été appelée
verify(orderRepositoryMock, never()).save(any(Order.class));
}
}
Dans cet exemple, `@SpringBootTest` charge le contexte de l'application. `@MockBean` remplace le bean `OrderRepository` par un mock. L'instance réelle de `OrderService` est injectée, mais elle reçoit le `orderRepositoryMock` à la place du vrai repository. On utilise ensuite `when().thenReturn()` pour définir le comportement du mock et `verify()` pour s'assurer que les interactions attendues ont bien eu lieu.
Cas d'usage courants et considérations
`@MockBean` est particulièrement utile dans les "slice tests" comme `@WebMvcTest`, `@DataJpaTest`, etc. Ces tests ne chargent qu'une partie spécifique du contexte Spring. Par exemple, dans un `@WebMvcTest`, seuls les composants liés à la couche Web (contrôleurs, filtres, etc.) sont chargés. Les dépendances comme les services ou repositories ne le sont pas par défaut et doivent souvent être mockées avec `@MockBean` pour que le contrôleur puisse fonctionner.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.mockito.Mockito.*;
// ... autres imports
@WebMvcTest(controllers = OrderController.class) // Teste seulement la couche Web pour OrderController
class OrderControllerTest {
@Autowired
private MockMvc mockMvc; // Pour simuler des requêtes HTTP
@MockBean // Le service est une dépendance du contrôleur, il faut le mocker
private OrderService orderServiceMock;
@Test
void getOrderById_shouldReturnOrder_whenOrderExists() throws Exception {
// Arrange
Order order = new Order();
order.setId(1L);
when(orderServiceMock.findOrderById(1L)).thenReturn(order);
// Act & Assert
mockMvc.perform(get("/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L));
verify(orderServiceMock).findOrderById(1L);
}
}
Il faut être conscient que l'utilisation de `@MockBean` peut affecter la mise en cache du contexte de test Spring. Chaque configuration unique de mocks (`@MockBean`) peut entraîner la création d'un nouveau contexte, ralentissant potentiellement l'exécution globale de la suite de tests. Utilisez `@MockBean` judicieusement, principalement lorsque l'isolation d'une dépendance gérée par Spring est nécessaire dans un test basé sur le contexte.
Par défaut, les mocks créés avec `@MockBean` sont réinitialisés après chaque méthode de test, vous n'avez donc généralement pas à vous soucier des interactions résiduelles d'un test précédent. C'est un avantage par rapport aux mocks gérés manuellement où la réinitialisation doit être explicite.