Introduction à JUnit en Java
Découvrez JUnit, comment l'ajouter à un projet Java et comment écrire votre premier test JUnit.
JUnit est le framework de référence pour écrire des tests automatisés en Java. Un test est simplement une petite méthode qui exécute une partie de votre code et vérifie qu'il s'est comporté comme prévu ; le rôle de JUnit est de découvrir ces méthodes, d'exécuter chacune d'elles de manière isolée, de vérifier les assertions et de signaler lesquelles ont réussi et lesquelles ont échoué. La génération actuelle, JUnit 5 (également appelée JUnit Jupiter), est fournie sous la forme d'un ensemble de petites bibliothèques à ajouter à votre build — elle ne fait pas partie du JDK — et elle alimente l'étape mvn test / gradle test qui conditionne l'intégration continue de presque tous les projets Java.
Pourquoi un framework de test
Vous pourriez vérifier votre code manuellement avec des méthodes main et println — mais cela ne passe pas à l'échelle. Un framework vous offre quatre choses que vous devriez autrement reconstruire vous-même :
- Découverte — il trouve automatiquement chaque méthode
@Test; vous n'avez jamais à maintenir une liste. - Isolation — chaque test dispose d'un contexte propre, de sorte qu'un test ne peut pas corrompre un autre.
- Assertions — un vocabulaire riche (
assertEquals,assertThrows, …) qui produit des messages d'échec précis. - Rapport — un résumé succès/échec uniforme que l'outil de build et l'IDE comprennent.
Faites cela une bonne fois et les tests deviennent peu coûteux à écrire, ce qui est tout l'intérêt : les tests peu coûteux sont effectivement écrits, et le code testé peut être modifié sans crainte.
Ajouter JUnit à un projet
JUnit 5 est une dépendance déclarée dans votre fichier de build. Avec Maven, l'agrégateur junit-jupiter inclut l'API (pour la compilation) et le moteur (pour l'exécution) :
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>Avec Gradle, ce sont deux lignes plus le commutateur useJUnitPlatform() :
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
}
test {
useJUnitPlatform()
}Les sources de test résident sous src/test/java, en miroir du package de la classe qu'elles testent. Le scope test maintient JUnit hors de votre artifact de production.
Votre premier test
Un test JUnit est une méthode ordinaire annotée avec @Test. À l'intérieur, vous appelez le code à tester et vérifiez le résultat. Voici une Calculator et une classe de test pour celle-ci :
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CalculatorTest {
private final Calculator calc = new Calculator();
@Test
void addReturnsSum() {
assertEquals(5, calc.add(2, 3));
}
@Test
void divideByZeroThrows() {
assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
}
}Remarquez le schéma que suit chaque test — Arrange un contexte, Act en appelant la méthode, Assert le résultat. Les méthodes de test sont package-private (pas besoin de public dans JUnit 5) et retournent void. Exécutez mvn test et le build passe au vert uniquement si toutes les assertions sont vérifiées.
Les annotations et assertions les plus utilisées
La surface d'API de JUnit est réduite. Ces quelques membres couvrent la grande majorité des tests réels :
| Membre | Package / classe | Rôle |
|---|---|---|
@Test | org.junit.jupiter.api | Marque une méthode comme test |
@BeforeEach / @AfterEach | org.junit.jupiter.api | S'exécute avant/après chaque test (mise en place / nettoyage des contextes) |
@BeforeAll / @AfterAll | org.junit.jupiter.api | S'exécute une seule fois avant/après tous les tests de la classe |
@DisplayName | org.junit.jupiter.api | Un nom lisible par l'humain pour les rapports |
@Disabled | org.junit.jupiter.api | Ignore temporairement un test |
assertEquals(exp, act) | Assertions | Échoue si les deux valeurs ne sont pas égales |
assertTrue / assertFalse | Assertions | Échoue si le boolean ne correspond pas |
assertThrows(type, exec) | Assertions | Échoue si le lambda ne lève pas cette exception |
assertNull / assertNotNull | Assertions | Échoue si la nullité est incorrecte |
@BeforeEach est ce qui garantit à chaque test un état propre — JUnit construit une nouvelle instance de la classe de test pour chaque @Test, puis exécute la mise en place, de sorte que l'état ne fuite jamais entre les tests.
Ce que JUnit fait pour vous, dans un fichier exécutable
Le code runner ici n'a pas JUnit dans son classpath (c'est une bibliothèque externe, pas une partie du JDK), donc l'exemple ci-dessous réimplémente la boucle principale de JUnit en Java pur : un contexte recréé avant chaque test, un petit ensemble d'assistants assertXxx, une liste de méthodes de test exécutées indépendamment, et un bilan succès/échec à la fin. C'est exactement la mécanique que JUnit automatise — la voir à nu rend le vrai framework évident. Un test échoue intentionnellement pour que vous puissiez voir à quoi ressemble le rouge.
Ce qu'il faut retenir de l'exécution :
- Les trois tests corrects affichent
PASSet le quatrième afficheFAIL deliberatelyFailing -> expected <10> but was <5>— un message d'échec précis, pas simplement "un test a échoué." Ce diff (expected … but was …) est exactement ce que leassertEqualsde JUnit vous donne, et c'est ce qui rend un test rouge diagnostiquable en un coup d'œil. setUp()s'exécute avant chaque test, donccalcest une nouvelleCalculatorà chaque fois. C'est le contrat de@BeforeEach: les tests sont isolés, et l'ordre dans lequel ils s'exécutent n'a jamais d'importance car aucun d'eux ne partage un état mutable.divideThrowsOnZeroréussit en affirmant qu'une exception est levée —assertThrowsfait de "cela devrait échouer" une assertion positive de première classe plutôt qu'un fragile try/catch. Les exceptions attendues sont un comportement qui mérite d'être testé, pas des erreurs à ignorer.- Le bilan final —
Tests run: 4, Passed: 3, Failed: 1, Assertions: 5— est le rapport. Un seul test qui échoue sur quatre fait passer l'ensemble du build enRED; l'IC traite tout échec comme un arrêt, c'est pourquoi une suite verte est significative. - Rien ici n'a importé JUnit, pourtant la forme est identique : découverte des méthodes annotées, mise en place par test, assertions, résumé. La valeur de JUnit est d'automatiser cette boucle (et d'ajouter la découverte, le parallélisme, les tests paramétrés et l'intégration IDE) afin que vous n'écriviez que les corps des tests.
Ce que couvre le reste de cette partie
Cette partie s'appuie sur la boucle principale que vous venez de voir :
- Les annotations JUnit —
@Test,@DisplayName,@Disabled, et le reste de l'ensemble des marqueurs. - Le cycle de vie du test — comment
@BeforeEach/@AfterEach/@BeforeAll/@AfterAlldonnent à chaque test un contexte propre. - Les assertions — le catalogue complet des
assertXxx, y comprisassertThrowspour tester les exceptions. - Les tests paramétrés — exécuter un corps de test sur de nombreuses entrées au lieu de copier-coller des cas.
- Les mocks avec Mockito — remplacer les collaborateurs d'une classe par des substituts pour qu'un test unitaire reste un test unitaire.
Si vous souhaitez avoir une vue d'ensemble de l'importance des tests automatisés avant de plonger dans l'API, consultez Les tests en Java. Sinon, le prochain chapitre commence là où commence toute suite : définir une classe de test et l'exécuter.