Introduzione a JUnit in Java
Cos'è JUnit, come aggiungerlo a un progetto Java e come scrivere il primo test JUnit.
JUnit è il framework standard de facto per la scrittura di test automatizzati in Java. Un test è semplicemente un piccolo metodo che esegue una parte del codice e asserisce che si sia comportato come previsto; il compito di JUnit è scoprire quei metodi, eseguire ciascuno in isolamento, controllare le asserzioni e segnalare quali sono passati e quali hanno fallito. La generazione attuale, JUnit 5 (chiamata anche JUnit Jupiter), viene distribuita come un insieme di piccole librerie da aggiungere alla build — non fa parte del JDK — e alimenta il passaggio mvn test / gradle test che funge da gate per quasi ogni CI di un progetto Java.
Perché usare un framework di testing
Potresti verificare il codice manualmente con metodi main e println — ma questo approccio non scala. Un framework ti fornisce quattro cose che altrimenti dovresti ricostruire da zero:
- Discovery — trova automaticamente ogni metodo
@Test; non devi mai mantenere una lista. - Isolation — ogni test riceve una fixture aggiornata, quindi un test non può corrompere un altro.
- Assertions — un vocabolario ricco (
assertEquals,assertThrows, …) che produce messaggi di errore precisi. - Reporting — un riepilogo uniforme pass/fail che lo strumento di build e l'IDE comprendono.
Implementare queste cose correttamente una volta rende i test economici da scrivere, che è l'obiettivo principale: i test economici vengono scritti, e il codice testato può essere modificato senza timore.
Aggiungere JUnit a un progetto
JUnit 5 è una dipendenza, dichiarata nel file di build. Con Maven, l'aggregatore junit-jupiter include l'API (per la compilazione) e il motore (per l'esecuzione):
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>Con Gradle sono due righe più lo switch useJUnitPlatform():
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
}
test {
useJUnitPlatform()
}I sorgenti di test risiedono in src/test/java, specchiando il package della classe che esercitano. Lo scope test mantiene JUnit fuori dall'artefatto di produzione.
Il primo test
Un test JUnit è un metodo ordinario annotato con @Test. Al suo interno si chiama il codice sotto test e si asserisce sul risultato. Ecco una classe Calculator e una classe di test per essa:
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));
}
}Nota il pattern che ogni test segue — Arrange (preparare una fixture), Act (chiamare il metodo), Assert (verificare il risultato). I metodi di test sono package-private (non serve public in JUnit 5) e restituiscono void. Esegui mvn test e la build diventa verde solo se ogni asserzione tiene.
Le annotazioni e le asserzioni più usate
La superficie di JUnit è ridotta. Questi pochi membri coprono la grande maggioranza dei test reali:
| Membro | Package / classe | Scopo |
|---|---|---|
@Test | org.junit.jupiter.api | Contrassegna un metodo come test |
@BeforeEach / @AfterEach | org.junit.jupiter.api | Eseguiti prima/dopo ogni test (setup / teardown delle fixture) |
@BeforeAll / @AfterAll | org.junit.jupiter.api | Eseguiti una volta prima/dopo tutti i test della classe |
@DisplayName | org.junit.jupiter.api | Un nome leggibile dall'uomo per i report |
@Disabled | org.junit.jupiter.api | Salta temporaneamente un test |
assertEquals(exp, act) | Assertions | Fallisce se i due valori non sono uguali |
assertTrue / assertFalse | Assertions | Fallisce se una condizione booleana non vale |
assertThrows(type, exec) | Assertions | Fallisce se la lambda non lancia quell'eccezione |
assertNull / assertNotNull | Assertions | Fallisce se la nullità non corrisponde |
@BeforeEach è ciò che dà ad ogni test una base pulita — JUnit crea una nuova istanza della classe di test per ogni @Test, poi esegue il setup, quindi lo stato non fuoriesce mai tra un test e l'altro.
Cosa fa JUnit per te, in un file eseguibile
Il code runner qui non ha JUnit nel suo classpath (è una libreria esterna, non parte del JDK), quindi l'esempio seguente reimplementa il ciclo principale di JUnit in Java puro: una fixture ricreata prima di ogni test, un piccolo insieme di helper assertXxx, un elenco di metodi di test eseguiti indipendentemente e un conteggio pass/fail alla fine. Questa è esattamente la macchina che JUnit automatizza — vederla nuda rende il framework reale ovvio. Un test fallisce deliberatamente così puoi vedere come appare il rosso.
Cosa trarre dall'esecuzione:
- I tre test corretti stampano
PASSe il quarto stampaFAIL deliberatelyFailing -> expected <10> but was <5>— un messaggio di errore preciso, non solo "un test ha fallito." Quella differenza (expected … but was …) è esattamente ciò cheassertEqualsdi JUnit fornisce, ed è ciò che rende un test rosso diagnosticabile a colpo d'occhio. setUp()viene eseguito prima di ogni test, quindicalcè unaCalculatornuova ogni volta. Questo è il contratto di@BeforeEach: i test sono isolati, e l'ordine in cui vengono eseguiti non può mai importare perché nessuno di essi condivide stato mutabile.divideThrowsOnZeropassa asserendo che un'eccezione viene lanciata —assertThrowsrende "questo dovrebbe fallire" una prima asserzione positiva invece di un fragile try/catch. Le eccezioni attese sono comportamenti degni di essere testati, non errori da ingoiare.- Il conteggio finale —
Tests run: 4, Passed: 3, Failed: 1, Assertions: 5— è il report. Un test fallito su quattro porta comunque l'intera build aRED; la CI tratta qualsiasi fallimento come uno stop, ecco perché una suite verde è significativa. - Nulla qui ha importato JUnit, eppure la forma è identica: discovery dei metodi annotati, setup per-test, asserzioni, riepilogo. Il valore di JUnit è che automatizza questo ciclo (e aggiunge discovery, parallelismo, test parametrizzati e integrazione con l'IDE) così scrivi solo i corpi dei test.
Cosa tratta il resto di questa parte
Questa parte si basa sul ciclo principale che hai appena visto:
- Annotazioni JUnit —
@Test,@DisplayName,@Disablede il resto del set di marker. - Il ciclo di vita del test — come
@BeforeEach/@AfterEach/@BeforeAll/@AfterAllforniscono ad ogni test una fixture pulita. - Asserzioni — il catalogo completo di
assertXxx, inclusoassertThrowsper i test delle eccezioni. - Test parametrizzati — eseguire un corpo di test su molti input invece di copiare e incollare i casi.
- Mocking con Mockito — sostituire i collaboratori di una classe con sostituti affinché un unit test rimanga un test unitario.
Se vuoi il quadro generale del perché i test automatizzati sono importanti prima di immergerti nell'API, consulta Testing in Java. Altrimenti il prossimo capitolo inizia dove ogni suite comincia: definire una classe di test ed eseguirla.