Tests unitaires avec JUnit 5
Un test unitaire vérifie qu'une petite unité de code (une méthode) produit le bon résultat dans toutes les situations. Ils détectent les régressions : les bugs introduits involontairement en modifiant du code existant.
Notions théoriques
Pourquoi tester ?
Sans tests automatisés, chaque modification du code nécessite de tout re-tester manuellement. Les tests unitaires permettent de :
- détecter les régressions immédiatement
- documenter le comportement attendu (le test dit ce que la méthode est censée faire)
- refactorer en confiance (si les tests passent, le comportement n'a pas changé)
Configuration Maven
Ajoutez ces dépendances dans pom.xml :
<dependencies>
<!-- JUnit 5 API -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 Engine (exécution) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
Structure d'un test JUnit 5
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class GuerrierTest {
private Guerrier guerrier;
@BeforeEach // exécuté avant chaque @Test
void setUp() {
guerrier = new Guerrier("Aragorn", 150, 30);
}
@AfterEach // exécuté après chaque @Test (nettoyage)
void tearDown() {
guerrier = null;
}
@Test
@DisplayName("attaquer() retourne la force du guerrier")
void attaquerReturnsForce() {
int degats = guerrier.attaquer();
assertEquals(150, degats);
}
@Test
@DisplayName("un guerrier avec force 0 ne fait pas de dégâts")
void attaquerAvecForceZero() {
Guerrier faible = new Guerrier("Faible", 0, 10);
assertEquals(0, faible.attaquer());
}
}
Assertions principales
assertEquals(valeurAttendue, valeurObtenue); // égalité
assertNotNull(objet); // non-null
assertNull(objet); // null
assertTrue(condition); // condition vraie
assertFalse(condition); // condition fausse
// Vérifier qu'une exception est levée
assertThrows(IllegalArgumentException.class, () -> {
new Guerrier("Test", -10, 5); // force négative → exception attendue
});
// Message d'erreur personnalisé (affiché si le test échoue)
assertEquals(150, guerrier.attaquer(), "La force du guerrier devrait être 150");
Mockito — Simuler des dépendances
Mockito crée des mocks (faux objets) qui remplacent les dépendances réelles (base de données, API externe...) pendant les tests.
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class PersonnageServiceTest {
@Mock
PersonnageRepository repo; // faux repository
@InjectMocks
PersonnageService service; // service à tester, avec le mock injecté
@Test
@DisplayName("trouver() retourne le personnage si présent")
void trouverRetournePersonnage() {
Guerrier guerrier = new Guerrier("Aragorn", 150, 30);
// Définir le comportement du mock
when(repo.findById(1L)).thenReturn(Optional.of(guerrier));
// Appeler le service
Optional<Personnage> result = service.trouver(1L);
// Vérifier le résultat
assertTrue(result.isPresent());
assertEquals("Aragorn", result.get().getNom());
}
@Test
@DisplayName("trouver() retourne Optional vide si absent")
void trouverRetourneVideSiAbsent() {
when(repo.findById(99L)).thenReturn(Optional.empty());
Optional<Personnage> result = service.trouver(99L);
assertTrue(result.isEmpty());
}
}
Lancer les tests
mvn test # lancer tous les tests
mvn test -Dtest=GuerrierTest # lancer une seule classe
IntelliJ IDEA affiche les résultats dans l'onglet "Run" avec des icônes vertes (succès) et rouges (échec). Cliquez sur un test rouge pour voir le message d'assertion.
Couverture de code avec JaCoCo
<!-- Dans pom.xml <plugins> -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals><goal>report</goal></goals>
</execution>
</executions>
</plugin>
Après mvn test, le rapport HTML est dans target/site/jacoco/index.html.
Exemple pratique
// Guerrier.java
public class Guerrier extends Personnage {
public Guerrier(String nom, int force, int defense) {
if (force < 0) throw new IllegalArgumentException("La force ne peut pas être négative");
this.nom = nom;
this.force = force;
this.defense = defense;
}
@Override
public int attaquer() {
return this.force;
}
public int recevoirDegats(int degats) {
int degatsReels = Math.max(0, degats - this.defense);
this.force -= degatsReels;
return degatsReels;
}
}
// GuerrierTest.java (dans src/test/java/)
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class GuerrierTest {
private Guerrier guerrier;
@BeforeEach
void setUp() {
guerrier = new Guerrier("Aragorn", 150, 30);
}
@Test
@DisplayName("attaquer() retourne la force du guerrier")
void attaquerReturnsForce() {
assertEquals(150, guerrier.attaquer());
}
@Test
@DisplayName("recevoirDegats() réduit la force selon la défense")
void recevoirDegatsReduitForce() {
int degatsReels = guerrier.recevoirDegats(50); // 50 - 30 défense = 20
assertEquals(20, degatsReels);
assertEquals(130, guerrier.getForce()); // 150 - 20 = 130
}
@Test
@DisplayName("force négative lève IllegalArgumentException")
void forceNegativeLanceException() {
assertThrows(IllegalArgumentException.class, () -> {
new Guerrier("Test", -1, 10);
});
}
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Vous allez écrire des tests unitaires pour le projet RPG.
Étape 1 — Tester attaquer() de Guerrier
Créez la classe GuerrierTest dans src/test/java/ et vérifiez que attaquer() retourne la force du guerrier.
@DisplayName("attaquer() retourne la force du guerrier") permet à JUnit d'afficher un message clair en cas d'échec. Préférez une phrase au présent qui décrit le comportement attendu plutôt que le nom de la méthode.
Étape 2 — Tester qu'une exception est levée
Vérifiez que créer un Guerrier avec une force négative lève bien une IllegalArgumentException.
Il est tentant de ne tester que le "chemin heureux" (la valeur correcte). Les tests les plus utiles sont ceux qui testent les cas limites (force=0, liste vide) et les cas d'erreur (valeur négative, null). Ce sont ces cas que l'on oublie souvent de gérer.
Étape 3 — Tester PersonnageService avec un mock
Créez un test pour PersonnageService.trouver(Long id) en mockant le PersonnageRepository.
Le test vérifie que result.isPresent() est vrai et que le nom est "Aragorn" — il teste le comportement observable du service. Il n'inspecte pas comment le service est implémenté en interne. Cette approche "boîte noire" permet de refactorer le service sans toucher les tests.