Test parametrizzati JUnit in Java
Esegui lo stesso test JUnit con input diversi usando @ParameterizedTest e sorgenti di valori.
Un test parametrizzato esegue lo stesso metodo di test più volte, una volta per ogni insieme di input fornito. Invece di copiare e incollare testReverseAbc, testReverseEmpty e testReverseSingle, scrivi la logica una volta sola e fornisci una sorgente di dati — un elenco di input e risultati attesi. JUnit 5 (il motore Jupiter) rende tutto ciò di prima classe con @ParameterizedTest e una famiglia di annotazioni sorgente. Il vantaggio è un minor numero di righe, una copertura più densa e ogni input riportato con il proprio esito pass/fail.
Questo capitolo presuppone che tu sappia già come si scrive e si asserisce un test semplice; in caso contrario, inizia con l'introduzione a JUnit e le asserzioni JUnit. Tratta quando ricorrere a un test parametrizzato, come scegliere una sorgente di argomenti (@ValueSource, @CsvSource, @MethodSource e altre) e l'errore più comune — un valore atteso sbagliato anziché un bug nel codice.
Da test ripetuti a un unico test parametrizzato
Un metodo @Test semplice testa esattamente uno scenario. Quando vuoi verificare lo stesso comportamento su una tabella di input, l'approccio ingenuo ripete il metodo:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@Test void two_isPrime() { assertTrue(Primes.isPrime(2)); }
@Test void seven_isPrime() { assertTrue(Primes.isPrime(7)); }
@Test void thirteen_isPrime() { assertTrue(Primes.isPrime(13)); }
}La versione parametrizzata riduce tutti e tre in un unico metodo. Si annota con @ParameterizedTest (non @Test) e si collega una sorgente che fornisce l'argomento per ogni esecuzione:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PrimesTest {
@ParameterizedTest
@ValueSource(ints = {2, 7, 13})
void isPrime(int candidate) {
assertTrue(Primes.isPrime(candidate));
}
}JUnit invoca isPrime tre volte — candidate=2, poi 7, poi 13 — e riporta tre risultati. Un valore fallito non nasconde gli altri.
Scegliere una sorgente di argomenti
L'annotazione @ParameterizedTest è inutile da sola; ha bisogno di una sorgente che produca gli argomenti. JUnit Jupiter ne include diverse, ognuna adatta a una diversa forma di dati:
| Sorgente | Fornisce | Ideale per |
|---|---|---|
@ValueSource | Un singolo letterale per esecuzione (ints, strings, doubles, …) | Test con un argomento |
@CsvSource | Una riga di valori separati da virgola per esecuzione | Poche righe inline con più colonne |
@CsvFileSource | Righe lette da un file .csv nel classpath | Tabelle grandi o gestite esternamente |
@MethodSource | Ciò che un metodo factory restituisce come Stream/Collection | Oggetti complessi, casi calcolati |
@EnumSource | Le costanti di un enum | Copertura esaustiva di un enum |
@NullSource / @EmptySource | Valori null e vuoti | Copertura dei casi limite per stringhe/collezioni |
La regola pratica: @ValueSource per un singolo input semplice, @CsvSource per una piccola tabella multicolonna, e @MethodSource quando i dati non entrano più nei letterali dell'annotazione.
Più colonne con @CsvSource
Quando ogni caso ha sia un input che un output atteso, @CsvSource ti offre una piccola tabella inline. Ogni stringa è una riga; le virgole la suddividono nei parametri del metodo in ordine:
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class StringsTest {
@ParameterizedTest
@CsvSource({
"abc, cba",
"racecar, racecar",
"'', ''" // single quotes denote an empty string
})
void reverse(String input, String expected) {
assertEquals(expected, Strings.reverse(input));
}
}JUnit converte ogni token separato da virgola nel tipo di parametro dichiarato, quindi @CsvSource({"4, 16"}) può essere passato a (int n, int square). Usa le virgolette singole per includere virgole o stringhe vuote all'interno di una cella.
Casi calcolati con @MethodSource
I valori delle annotazioni devono essere costanti a tempo di compilazione, quindi una volta che gli argomenti sono oggetti reali o richiedono calcoli, passa a @MethodSource. Questo nomina un metodo statico che restituisce uno Stream<Arguments> (o qualsiasi Collection/array):
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TaxTest {
static Stream<Arguments> brackets() {
return Stream.of(
Arguments.of(0, 0.0),
Arguments.of(10_000, 1_000.0),
Arguments.of(50_000, 7_500.0)
);
}
@ParameterizedTest(name = "income {0} -> tax {1}")
@MethodSource("brackets")
void computesTax(int income, double expectedTax) {
assertEquals(expectedTax, Tax.of(income));
}
}L'attributo name opzionale personalizza come ogni invocazione appare nel report di test, con {0}, {1} al posto degli argomenti — prezioso quando una singola riga fallita deve essere identificata a colpo d'occhio.
Un esempio pratico: un runner parametrizzato senza JUnit
Il code runner non ha JUnit nel suo classpath, quindi questo programma modella il meccanismo che un test parametrizzato incarna con puro codice JDK: un singolo controllo è definito una volta, poi eseguito su un elenco di casi — esattamente ciò che fa @ParameterizedTest dietro le annotazioni. Un caso è deliberatamente sbagliato così puoi vedere come le singole righe passano o falliscono in isolamento.
Cosa ricavare dall'esecuzione:
- Il blocco
reversestampa quattro righePASSe>> reverse: 4 passed, 0 failed— un corpo (reverse) è stato eseguito contro quattro righe, rispecchiando come un singolo metodo@ParameterizedTestviene invocato una volta per ogni riga di@CsvSource. - Il blocco
isPrimestampaPASSper gli input2,7,9e1, maFAILper l'input4, perchéisPrime(4)restituiscefalsementre la riga affermavatrue— un'aspettativa sbagliata, non un bug nel codice, che è l'errore più comune nei test parametrizzati. - Quel singolo fallimento è riportato sulla propria riga e conteggiato come
>> isPrime: 4 passed, 1 failed; le altre righe passano comunque, dimostrando il vantaggio chiave rispetto a un ciclo scritto a mano con un'unica asserzione — ogni input è un caso indipendente, riportato individualmente. - L'helper
runAllprende l'unità comeFunctione i casi comeList, separando la logica sotto test dai dati — esattamente la separazione che@ParameterizedTestpiù una sorgente di argomenti offre. - Ogni riga mostra
expectedaccanto adactual, quindi la riga4 / expected=true / actual=falseindica esattamente quale valore è discordante — lo stesso valore diagnostico fornito dal messaggio diassertEqualsdi JUnit e dal templatename = "...".
Quando usare un test parametrizzato
Ricorri a @ParameterizedTest quando un comportamento deve valere per una tabella di input — valori limite, classi di equivalenza o un elenco di regressione di input che in passato hanno causato problemi. Continua a usare un semplice @Test quando uno scenario richiede una configurazione unica o asserzioni distinte; ammassare casi non correlati in un unico metodo parametrizzato rende solo il report più difficile da leggere. Per la configurazione condivisa tra entrambi gli stili, vedi il capitolo sul ciclo di vita dei test, e per il vocabolario completo delle asserzioni usate all'interno di ogni esecuzione, il capitolo sulle asserzioni.