Pourquoi Tester Spring Boot avec JUnit ?
Dans un environnement de développement moderne, tester est une pratique cruciale qui permet non seulement d'assurer la qualité du code mais aussi de prévenir les bugs avant qu'ils ne puissent atteindre l'environnement de production. Avec Spring Boot, une plateforme populaire pour le développement de microservices et d'applications web, l'utilisation de JUnit pour le testing devient indispensable.
Un cas d'usage concret : Imaginez un système de gestion des utilisateurs qui nécessite des fonctionnalités telles que la création, la mise à jour, la suppression et la récupération d'utilisateurs. Sans tests unitaires, il serait difficile de s'assurer que chaque fonctionnalité fonctionne correctement sans causer des erreurs lors de l'utilisation du système.
Prerequis
Pour suivre ce tutoriel, vous aurez besoin des éléments suivants :
- Java JDK 11 ou ultérieur
- Apache Maven ou Gradle pour la gestion des dépendances et le build
- Un éditeur de code (IntelliJ IDEA, Eclipse, VS Code)
- Connaissance de base en Spring Boot
Concepts fondamentaux
1. Qu'est-ce que JUnit ?
JUnit est un framework de test Java qui permet d'écrire et d'exécuter des tests unitaires pour vérifier le bon fonctionnement du code. Il facilite la création de méthodes de test, leur exécution et l'affichage des résultats.
// Exemple de classe de test JUnit
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class UserServiceTest {
@Test
public void testCreateUser() {
// Création d'une instance du service utilisateur
UserService userService = new UserService();
// Création d'un utilisateur et appel de la méthode createUser
User user = userService.createUser("John Doe", "john.doe@example.com");
// Vérification que l'utilisateur a été créé avec succès
assertEquals("John Doe", user.getName());
assertEquals("john.doe@example.com", user.getEmail());
}
}
2. Annotatifs JUnit
JUnit utilise des annotations pour décrire les méthodes de test et leur comportement. Voici quelques-unes des annotations les plus courantes :
@Test: Indique que la méthode est une méthode de test.@BeforeEach: Exécute avant chaque méthode de test.@AfterEach: Exécute après chaque méthode de test.@BeforeAll: Exécute une seule fois avant toutes les méthodes de test.@AfterAll: Exécute une seule fois après toutes les méthodes de test.
// Exemple d'utilisation des annotations @BeforeEach et @AfterEach
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
public class UserServiceTest {
private UserService userService;
@BeforeEach
public void setUp() {
// Initialisation du service utilisateur avant chaque test
userService = new UserService();
}
@AfterEach
public void tearDown() {
// Nettoyage après chaque test
userService = null;
}
@Test
public void testCreateUser() {
User user = userService.createUser("John Doe", "john.doe@example.com");
assertEquals("John Doe", user.getName());
assertEquals("john.doe@example.com", user.getEmail());
}
}
3. Assertions JUnit
JUnit propose une variété d'assertions pour vérifier les conditions dans les méthodes de test :
assertEquals(expected, actual): Vérifie que la valeur attendue est égale à la valeur réelle.assertTrue(condition): Vérifie que la condition est vraie.assertFalse(condition): Vérifie que la condition est fausse.assertNull(value): Vérifie que la valeur est null.
// Exemple d'utilisation des assertions JUnit
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class UserServiceTest {
@Test
public void testUpdateUser() {
User user = new User("John Doe", "john.doe@example.com");
userService.updateUser(user);
assertEquals("Updated John Doe", user.getName());
assertTrue(user.isActive());
}
}
4. Mocking avec Mockito
Mockito est un framework de mocking Java qui permet de créer des objets simulés (mocks) pour tester les classes en isolant leurs dépendances.
// Exemple d'utilisation de Mockito pour le mocking
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
public class UserServiceTest {
@Test
public void testDeleteUser() {
// Création d'un mock du repository utilisateur
UserRepository userRepository = mock(UserRepository.class);
UserService userService = new UserService(userRepository);
User user = new User("John Doe", "john.doe@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
userService.deleteUser(1L);
verify(userRepository, times(1)).deleteById(1L);
}
}
Mise en pratique : projet fil rouge
Nous allons créer un mini-projet complet et réaliste : un gestionnaire de tâches. Ce projet comprendra les fonctionnalités suivantes :
- Ajouter une tâche
- Mettre à jour une tâche
- Supprimer une tâche
- Récupérer toutes les tâches
Étape 1 : Création du modèle Task
// src/main/java/com/example/todo/Task.java
package com.example.todo;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
private boolean completed;
// Getters and Setters
}
Étape 2 : Création du service TaskService
// src/main/java/com/example/todo/TaskService.java
package com.example.todo;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class TaskService {
private List<Task> tasks = new ArrayList<>();
public Task addTask(Task task) {
tasks.add(task);
return task;
}
public Task updateTask(Long id, Task updatedTask) {
for (int i = 0; i < tasks.size(); i++) {
if (tasks.get(i).getId().equals(id)) {
tasks.set(i, updatedTask);
break;
}
}
return updatedTask;
}
public void deleteTask(Long id) {
tasks.removeIf(task -> task.getId().equals(id));
}
public List<Task> getAllTasks() {
return new ArrayList<>(tasks);
}
public Optional<Task> getTaskById(Long id) {
return tasks.stream()
.filter(task -> task.getId().equals(id))
.findFirst();
}
}
Étape 3 : Création du contrôleur TaskController
// src/main/java/com/example/todo/TaskController.java
package com.example.todo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/tasks")
public class TaskController {
@Autowired
private TaskService taskService;
@PostMapping
public Task addTask(@RequestBody Task task) {
return taskService.addTask(task);
}
@PutMapping("/{id}")
public Task updateTask(@PathVariable Long id, @RequestBody Task updatedTask) {
return taskService.updateTask(id, updatedTask);
}
@DeleteMapping("/{id}")
public void deleteTask(@PathVariable Long id) {
taskService.deleteTask(id);
}
@GetMapping
public List<Task> getAllTasks() {
return taskService.getAllTasks();
}
@GetMapping("/{id}")
public Optional<Task> getTaskById(@PathVariable Long id) {
return taskService.getTaskById(id);
}
}
Étape 4 : Création du service de test TaskServiceTest
// src/test/java/com/example/todo/TaskServiceTest.java
package com.example.todo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class TaskServiceTest {
private TaskService taskService;
@BeforeEach
public void setUp() {
taskService = new TaskService();
}
@Test
public void testAddTask() {
Task task = new Task("Buy groceries", false);
Task result = taskService.addTask(task);
assertEquals("Buy groceries", result.getDescription());
assertFalse(result.isCompleted());
}
@Test
public void testUpdateTask() {
Task task = new Task("Buy groceries", false);
taskService.addTask(task);
Task updatedTask = new Task("Buy groceries and milk", true);
Task result = taskService.updateTask(1L, updatedTask);
assertEquals("Buy groceries and milk", result.getDescription());
assertTrue(result.isCompleted());
}
@Test
public void testDeleteTask() {
Task task = new Task("Buy groceries", false);
taskService.addTask(task);
taskService.deleteTask(1L);
List<Task> tasks = taskService.getAllTasks();
assertEquals(0, tasks.size());
}
@Test
public void testGetAllTasks() {
Task task1 = new Task("Buy groceries", false);
Task task2 = new Task("Clean the house", false);
taskService.addTask(task1);
taskService.addTask(task2);
List<Task> tasks = taskService.getAllTasks();
assertEquals(2, tasks.size());
}
@Test
public void testGetTaskById() {
Task task = new Task("Buy groceries", false);
taskService.addTask(task);
Optional<Task> result = taskService.getTaskById(1L);
assertTrue(result.isPresent());
assertEquals("Buy groceries", result.get().getDescription());
}
}
Étape 5 : Création du contrôleur de test TaskControllerTest
// src/test/java/com/example/todo/TaskControllerTest.java
package com.example.todo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
public class TaskControllerTest extends IntegrationTestBase {
@BeforeEach
public void setUp() {
// Initialisation des données avant chaque test
}
@Test
public void testAddTask() throws Exception {
mockMvc.perform(post("/tasks")
.contentType("application/json")
.content("{\"description\":\"Buy groceries\",\"completed\":false}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.description").value("Buy groceries"))
.andExpect(jsonPath("$.completed").value(false));
}
@Test
public void testUpdateTask() throws Exception {
mockMvc.perform(put("/tasks/1")
.contentType("application/json")
.content("{\"description\":\"Buy groceries and milk\",\"completed\":true}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.description").value("Buy groceries and milk"))
.andExpect(jsonPath("$.completed").value(true));
}
@Test
public void testDeleteTask() throws Exception {
mockMvc.perform(delete("/tasks/1"))
.andExpect(status().isNoContent());
}
@Test
public void testGetAllTasks() throws Exception {
mockMvc.perform(get("/tasks"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.[0].description").value("Buy groceries"))
.andExpect(jsonPath("$.[0].completed").value(false));
}
@Test
public void testGetTaskById() throws Exception {
mockMvc.perform(get("/tasks/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.description").value("Buy groceries"))
.andExpect(jsonPath("$.completed").value(false));
}
}
Étape 6 : Configuration de la base de données en mémoire
// src/test/resources/application.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
Erreurs frequentes et debugging
1. java.lang.NoClassDefFoundError: org/mockito/Mockito
Cette erreur se produit lorsque Mockito n'est pas correctement ajouté à votre projet.
## ❌ Mauvais
// pom.xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
<scope>test</scope>
</dependency>
## ✅ Correct
// pom.xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.0.0</version>
<scope>test</scope>
</dependency>
2. java.lang.AssertionError: Expected :false, but was :true
Cette erreur se produit lorsque l'assertion assertTrue échoue.
## ❌ Mauvais
// src/test/java/com/example/todo/TaskServiceTest.java
@Test
public void testUpdateTask() {
Task task = new Task("Buy groceries", false);
taskService.addTask(task);
Task updatedTask = new Task("Buy groceries and milk", true);
Task result = taskService.updateTask(1L, updatedTask);
assertFalse(result.isCompleted());
}
## ✅ Correct
// src/test/java/com/example/todo/TaskServiceTest.java
@Test
public void testUpdateTask() {
Task task = new Task("Buy groceries", false);
taskService.addTask(task);
Task updatedTask = new Task("Buy groceries and milk", true);
Task result = taskService.updateTask(1L, updatedTask);
assertTrue(result.isCompleted());
}
3. java.lang.NullPointerException: Cannot invoke "com.example.todo.TaskService.deleteTask(java.lang.Long)" because "this.taskService" is null
Cette erreur se produit lorsque le service n'est pas correctement injecté dans le contrôleur de test.
// src/test/java/com/example/todo/TaskControllerTest.java
@Autowired
private TaskService taskService; // Ajouter @Autowired pour l'injection du service
@BeforeEach
public void setUp() {
taskService = new TaskService(); // Supprimer cette ligne
}
Pour aller plus loin
Tests d'intégration : Apprenez comment tester les interactions entre différentes couches et composants de votre application.
Tests end-to-end (E2E) : Utilisez Selenium pour écrire des tests qui simulent une interaction complète avec l'application.
Coverage et rapports de test : Configurez un outil pour générer des rapports de couverture de code et analyser les résultats.
Défi pratique
Créez un gestionnaire simple d'utilisateurs qui permet de créer, mettre à jour, supprimer et récupérer des utilisateurs. Assurez-vous d'écrire des tests unitaires pour chacune des fonctionnalités.
Indice : Utilisez une classe User avec les attributs id, name, et email. Créez un service UserService avec des méthodes correspondantes, puis écrivez des tests unitaires pour vérifier le bon fonctionnement de chaque méthode.