Ciclo di vita dei test JUnit in Java
Ciclo di vita delle istanze e comportamento per metodo vs. per classe in JUnit 5.
Ogni test JUnit 5 viene eseguito all'interno di un ciclo di vita ben definito: una sequenza di hook di setup e teardown che si attivano intorno ai metodi @Test in un ordine garantito. Comprendere quell'ordine — e la regola secondo cui JUnit crea una nuova istanza della classe di test per ogni metodo di test — è ciò che distingue suite di test fragili e dipendenti dall'ordine da quelle pulite e isolate. Questo capitolo illustra le cinque annotazioni del ciclo di vita e i due cicli di vita delle istanze offerti da JUnit.
Le cinque annotazioni del ciclo di vita
JUnit 5 (il package org.junit.jupiter.api) definisce quattro annotazioni di callback che racchiudono i test, più @Test stesso. Per una panoramica più completa di ciascuna, vedere annotazioni JUnit; se sei nuovo al framework, inizia con l'introduzione a JUnit.
| Annotazione | Viene eseguita | Il metodo deve essere |
|---|---|---|
@BeforeAll | Una volta, prima di qualsiasi test nella classe | static (nel ciclo di vita predefinito) |
@BeforeEach | Prima di ogni metodo @Test | istanza |
@Test | Il test stesso | istanza |
@AfterEach | Dopo ogni metodo @Test | istanza |
@AfterAll | Una volta, dopo che tutti i test sono stati eseguiti | static (nel ciclo di vita predefinito) |
Una singola classe di test con tre test attiva quindi @BeforeAll una volta, poi @BeforeEach → @Test → @AfterEach tre volte, quindi @AfterAll una volta.
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"); }
}Una nuova istanza per ogni metodo di test
La regola più importante del ciclo di vita: per impostazione predefinita JUnit costruisce una nuova istanza della classe di test prima di ogni metodo di test. I campi che si modificano in un test non possono influenzare un altro, perché il test successivo viene eseguito su un oggetto diverso. Questo è ciò che rende i test indipendenti dall'ordine di esecuzione.
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
}Entrambi i test vedono counter == 1. Se JUnit riutilizzasse un'unica istanza, il secondo test osserverebbe 2 e passerebbe o fallirebbe in base all'ordine — esattamente la fragilità che questo design previene.
PER_METHOD vs. PER_CLASS
È possibile rinunciare all'istanza per metodo con @TestInstance(Lifecycle.PER_CLASS). In tal caso JUnit crea una sola istanza per l'intera classe, i campi di istanza persistono tra i test e — per comodità — @BeforeAll/@AfterAll possono essere non-static.
| Aspetto | PER_METHOD (predefinito) | PER_CLASS |
|---|---|---|
| Istanze create | una per @Test | una per classe |
| Stato dei campi di istanza | resettato ad ogni test | condiviso tra i test |
@BeforeAll/@AfterAll | devono essere static | possono essere metodi di istanza |
| Adatto per | massimo isolamento | setup condiviso costoso |
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() { }
}Ricorrere a PER_CLASS solo quando il setup è genuinamente costoso e sicuro da condividere. Il valore predefinito fornisce isolamento gratuitamente.
Le asserzioni sono il modo in cui un test segnala un fallimento
Un ciclo di vita esiste per eseguire asserzioni. Assertions.assertEquals(expected, actual) lancia un AssertionFailedError quando i valori differiscono, il che interrompe quel singolo test (il suo @AfterEach viene comunque eseguito) e lo segna come fallito — gli altri test continuano. Vedere asserzioni JUnit per l'insieme completo dei metodi assert*.
import static org.junit.jupiter.api.Assertions.*;
@Test void example() {
assertEquals(42, compute());
assertTrue(isReady());
assertThrows(IllegalArgumentException.class, () -> parse("bad"));
}Un esempio pratico: tracciare il ciclo di vita a mano
Non è disponibile un runner JUnit in questo playground, quindi il programma seguente modella il ciclo di vita con codice JDK semplice: attiva gli hook nell'ordine di JUnit, confronta PER_METHOD (una nuova istanza per test) con PER_CLASS (un'unica istanza condivisa) e termina con un piccolo harness di auto-verifica nello spirito di assertEquals.
Cosa trarre dall'esecuzione:
- Il blocco
PER_METHODstampainstance#1,instance#2,instance#3per i tre test, dimostrando la regola predefinita di JUnit: una nuova istanza di test viene costruita per ogni metodo@Test, quindi nessun test può vedere lo stato modificato di un altro test. - In
PER_METHODogni riga[TEST]riportacounter=1, mai2o3. Ogni istanza ha ottenuto il proprio campo fresco, il che è il motivo per cui i test rimangono indipendenti dall'ordine di esecuzione — il vantaggio principale del ciclo di vita predefinito. - Il blocco
PER_CLASSriutilizzainstance#1per tutti e tre i test, e il suocountersale da1 → 2 → 3. Con un'unica istanza condivisa, lo stato dei campi di istanza si propaga deliberatamente tra i test — utile per fixture condivise costose, pericoloso se lo si dimentica. @BeforeAlle@AfterAllcompaiono ciascuno esattamente una volta per blocco, racchiudendo le coppie@BeforeEach/@AfterEachper test che si attivano tre volte — l'esatto ordine di annidamento che JUnit garantisce intorno ai test.- L'harness finale stampa
PASS:per tutti e tre i controlli; uncheckfallito lancia unAssertionErrorcon un messaggioFAIL:, rispecchiando il modo in cuiAssertions.assertEqualsinterrompe un singolo test con unAssertionFailedErrorlasciando gli altri in esecuzione.
Capitoli correlati
- Introduzione a JUnit — configurare JUnit 5 ed eseguire il primo test.
- Annotazioni JUnit — ogni annotazione che definisce il ciclo di vita.
- Asserzioni JUnit — come un test segnala effettivamente esito positivo o negativo.
- Test parametrizzati — eseguire un corpo di test su molti input.