Cycle de vie des tests JUnit en Java
Cycle de vie des instances de test et comportement par méthode ou par classe dans JUnit 5.
Chaque test JUnit 5 s'exécute dans un cycle de vie bien défini : une séquence de hooks d'initialisation et de nettoyage qui se déclenchent autour de vos méthodes @Test dans un ordre garanti. Comprendre cet ordre — et la règle selon laquelle JUnit crée une nouvelle instance de la classe de test pour chaque méthode de test — est ce qui distingue des suites de tests fragiles et dépendantes de l'ordre d'exécution des suites propres et isolées. Ce chapitre présente les cinq annotations du cycle de vie et les deux modes de cycle de vie que JUnit propose.
Les cinq annotations du cycle de vie
JUnit 5 (le package org.junit.jupiter.api) définit quatre annotations de rappel qui encadrent vos tests, ainsi que @Test lui-même. Pour un tour d'horizon complet de chacune, consultez les annotations JUnit ; si vous débutez avec ce framework, commencez par l'introduction à JUnit.
| Annotation | S'exécute | La méthode doit être |
|---|---|---|
@BeforeAll | Une fois, avant tout test de la classe | static (dans le cycle de vie par défaut) |
@BeforeEach | Avant chaque méthode @Test | instance |
@Test | Le test lui-même | instance |
@AfterEach | Après chaque méthode @Test | instance |
@AfterAll | Une fois, après que tous les tests ont été exécutés | static (dans le cycle de vie par défaut) |
Une classe de test unique contenant trois tests déclenche donc @BeforeAll une fois, puis @BeforeEach → @Test → @AfterEach trois fois, puis @AfterAll une fois.
import org.junit.jupiter.api.*;
class CalculatorTest {
@BeforeAll static void initSuite() { System.out.println("once, up front"); }
@BeforeEach void setUp() { System.out.println("before each test"); }
@Test void add() { Assertions.assertEquals(4, 2 + 2); }
@Test void subtract() { Assertions.assertEquals(0, 2 - 2); }
@AfterEach void tearDown() { System.out.println("after each test"); }
@AfterAll static void close() { System.out.println("once, at the end"); }
}Une nouvelle instance par méthode de test
La règle la plus importante du cycle de vie : par défaut, JUnit construit une toute nouvelle instance de la classe de test avant chaque méthode de test. Les champs que vous modifiez dans un test ne peuvent pas se propager dans un autre, car le test suivant s'exécute sur un objet différent. C'est ce qui rend les tests indépendants de l'ordre d'exécution.
class IsolationTest {
private int counter = 0; // re-initialised for every test
@Test void first() { counter++; Assertions.assertEquals(1, counter); }
@Test void second() { counter++; Assertions.assertEquals(1, counter); } // also 1, not 2
}Les deux tests voient counter == 1. Si JUnit réutilisait une seule instance, le second test observerait 2 et passerait ou échouerait selon l'ordre — exactement la fragilité que cette conception prévient.
PER_METHOD vs. PER_CLASS
Vous pouvez désactiver le comportement par instance par méthode avec @TestInstance(Lifecycle.PER_CLASS). JUnit crée alors une seule instance pour toute la classe, les champs d'instance persistent entre les tests, et — par commodité — @BeforeAll/@AfterAll peuvent ne pas être static.
| Aspect | PER_METHOD (par défaut) | PER_CLASS |
|---|---|---|
| Instances créées | une par @Test | une par classe |
| État des champs d'instance | réinitialisé à chaque test | partagé entre les tests |
@BeforeAll/@AfterAll | doivent être static | peuvent être des méthodes d'instance |
| Idéal pour | isolation maximale | initialisation partagée coûteuse |
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.TestInstance.Lifecycle;
@TestInstance(Lifecycle.PER_CLASS)
class SharedFixtureTest {
@BeforeAll void openConnection() { /* non-static is now legal */ }
@AfterAll void closeConnection() { }
}Utilisez PER_CLASS uniquement lorsque l'initialisation est véritablement coûteuse et sûre à partager. Le mode par défaut vous offre l'isolation gratuitement.
Les assertions permettent à un test de signaler un échec
Un cycle de vie existe pour exécuter des assertions. Assertions.assertEquals(expected, actual) lève une AssertionFailedError lorsque les valeurs diffèrent, ce qui interrompt ce test unique (son @AfterEach s'exécute tout de même) et le marque comme échoué — les autres tests continuent. Consultez les assertions JUnit pour la liste complète des méthodes assert*.
import static org.junit.jupiter.api.Assertions.*;
@Test void example() {
assertEquals(42, compute());
assertTrue(isReady());
assertThrows(IllegalArgumentException.class, () -> parse("bad"));
}Un exemple concret : tracer le cycle de vie manuellement
Il n'y a pas de runner JUnit dans cet environnement de code, donc le programme ci-dessous modélise le cycle de vie en code JDK pur : il déclenche les hooks dans l'ordre de JUnit, compare PER_METHOD (une nouvelle instance par test) avec PER_CLASS (une instance partagée), et se termine par un petit harnais d'auto-vérification dans l'esprit de assertEquals.
Ce qu'il faut retenir de l'exécution :
- Le bloc
PER_METHODafficheinstance#1,instance#2,instance#3pour les trois tests, prouvant la règle par défaut de JUnit : une nouvelle instance de test est construite pour chaque méthode@Test, donc aucun test ne peut voir l'état modifié d'un autre test. - Dans
PER_METHOD, chaque ligne[TEST]affichecounter=1, jamais2ou3. Chaque instance dispose de son propre champ fraîchement initialisé, ce qui explique pourquoi les tests restent indépendants de l'ordre d'exécution — le principal avantage du cycle de vie par défaut. - Le bloc
PER_CLASSréutiliseinstance#1pour les trois tests, et soncountermonte de1 → 2 → 3. Avec une seule instance partagée, l'état des champs d'instance se propage délibérément entre les tests — utile pour des fixtures partagées coûteuses, dangereux si on l'oublie. @BeforeAllet@AfterAllapparaissent chacun exactement une fois par bloc, encadrant les paires@BeforeEach/@AfterEachpar test qui se déclenchent trois fois — l'ordre d'imbrication exact que JUnit garantit autour de vos tests.- Le harnais final affiche
PASS:pour les trois vérifications ; uncheckéchoué lève uneAssertionErroravec un messageFAIL:, reproduisant la façon dontAssertions.assertEqualsinterrompt un seul test avec uneAssertionFailedErrortout en laissant les autres s'exécuter.
Chapitres connexes
- Introduction à JUnit — configurer JUnit 5 et exécuter votre premier test.
- Annotations JUnit — toutes les annotations qui façonnent le cycle de vie.
- Assertions JUnit — comment un test signale réellement un succès ou un échec.
- Tests paramétrés — exécuter un corps de test sur plusieurs entrées.