Test parametrizzati consentono agli sviluppatori di testare efficientemente il loro codice con una gamma di valori di input. Nel campo dei test JUnit, gli utenti esperti da tempo si sono confrontati con la complessità dell’implementazione di questi test. Ma con il rilascio di JUnit 5.7, una nuova era di test parametrizzati entra in scena, offrendo agli sviluppatori supporto di prim’ordine e capacità avanzate. Esploriamo le affascinanti possibilità che JUnit 5.7 porta sul tavolo per i test parametrizzati!
Campioni di Parametrizzazione Dalla Documentazione di JUnit 5.7
Vediamo alcuni esempi dalla documentazione:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
L’annotazione @ParameterizedTest
deve essere accompagnata da una delle tante annotazioni di origine fornite che descrivono da dove prendere i parametri. La fonte dei parametri è spesso detta “fornitore di dati”.
I will not dive into their detailed description here: the JUnit user guide does it better than I could, but allow me to share several observations:
- L’
@ValueSource
è limitato a fornire un solo valore di parametro. In altre parole, il metodo di test non può avere più di un argomento, e i tipi che si possono utilizzare sono limitati anch’essi. - Il passaggio di più argomenti è parzialmente affrontato da
@CsvSource
, analizzando ogni stringa in un record che viene poi passato come argomenti campo per campo. Questo può diventare difficile da leggere con stringhe lunghe e/o un gran numero di argomenti. Anche i tipi che si possono utilizzare sono limitati — ne parleremo più avanti. - Tutte le fonti che dichiarano i valori effettivi nelle annotazioni sono limitate a valori che sono costanti a tempo di compilazione (limitazione delle annotazioni Java, non JUnit).
@MethodSource
e@ArgumentsSource
forniscono un flusso/collezione di n-uple (non tipizzate) che vengono poi passate come argomenti del metodo. Vengono supportati vari tipi effettivi per rappresentare la sequenza di n-uple, ma nessuno di essi garantisce che si adatteranno all’elenco di argomenti del metodo. Questo tipo di sorgente richiede metodi o classi aggiuntivi, ma non impone restrizioni su dove e come ottenere i dati di test.
Comme puoi vedere, i tipi di sorgente disponibili vanno da quelli semplici (facili da usare, ma limitati in funzionalità) a quelli estremamente flessibili che richiedono più codice per funzionare.
- Nota a margine — Questo è generalmente un segno di un buon design: un po’ di codice è necessario per la funzionalità essenziale, e l’aggiunta di complessità aggiuntiva è giustificata quando utilizzata per abilitare un caso d’uso più esigente.
Ciò che non sembra adattarsi a questo ipotetico continuum da semplice a flessibile, è @EnumSource
. Guarda questo esempio non banale di quattro set di parametri con 2 valori ciascuno.
- Nota — Mentre
@EnumSource
passa il valore dell’enum come singolo parametro del metodo di test, concettualmente, il test è parametrizzato dai campi dell’enum, che non impone restrizioni sul numero di parametri.
enum Direction {
UP(0, '^'),
RIGHT(90, '>'),
DOWN(180, 'v'),
LEFT(270, '<');
private final int degrees;
private final char ch;
Direction(int degrees, char ch) {
this.degrees = degrees;
this.ch = ch;
}
}
@ParameterizedTest
@EnumSource
void direction(Direction dir) {
assertEquals(0, dir.degrees % 90);
assertFalse(Character.isWhitespace(dir.ch));
int orientation = player.getOrientation();
player.turn(dir);
assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
}
Pensa soltanto: la lista di valori hard-coded limita gravemente la sua flessibilità (nessun dato esterno o generato), mentre la quantità di codice aggiuntivo necessario per dichiarare l’enum
rende questa opzione piuttosto verbosa rispetto, diciamo, a @CsvSource
.
Ma questa è solo una prima impressione. Vedremo quanto può diventare elegante questa soluzione quando si utilizza la vera potenza degli enum Java.
- Nota a margine: Questo articolo non tratta della verifica degli enum che sono parte del tuo codice di produzione. Questi, ovviamente, dovevano essere dichiarati indipendentemente da come scegli di verificarli. Invece, si concentra su quando e come esprimere i tuoi dati di test sotto forma di enum.
Quando Utilizzarlo
Ci sono situazioni in cui gli enum performed meglio delle alternative:
Parametri Multipli per Test
Quando tutto ciò di cui hai bisogno è un singolo parametro, probabilmente non vuoi complicare le cose oltre @ValueSource
. Ma appena hai bisogno di più parametri -— diciamo, input e risultati attesi -— devi ricorrere a @CsvSource,
@MethodSource/@ArgumentsSource
o @EnumSource
.
In un certo senso, l’enum
ti permette di “intasare” qualsiasi numero di campi di dati.
Quindi, quando hai bisogno di aggiungere più parametri al metodo di test in futuro, aggiungi semplicemente più campi nei tuoi enum esistenti, lasciando invariata la firma del metodo di test. Questo diventa prezioso quando riutilizzi il tuo provider di dati in più test.
Per altre fonti, è necessario utilizzare ArgumentsAccessor
s o ArgumentsAggregator
s per la flessibilità che gli enum hanno fuori dagli schemi.
Sicurezza di tipo
Per gli sviluppatori Java, questo dovrebbe essere un grande vantaggio.
Parametri letti da CSV (file o letterali), @MethodSource
o @ArgumentsSource
, non forniscono alcuna garanzia a tempo di compilazione che il numero di parametri e i loro tipi corrisponderanno alla firma.
Ovviamente, JUnit si lamenta a tempo di esecuzione ma dimenticatevi di qualsiasi assistenza del codice dal vostro IDE.
Stesso discorso del prima, questo si somma quando riutilizzate gli stessi parametri per più test. Utilizzare un approccio a prova di tipo sarebbe un grande vantaggio quando si estende il set di parametri in futuro.
Tipi personalizzati
Questo è principalmente un vantaggio rispetto alle fonti basate su testo, come quelle che leggono dati da CSV — i valori codificati nel testo devono essere convertiti in tipi Java.
Se avete una classe personalizzata da istanziare dal record CSV, potete farlo utilizzando ArgumentsAggregator
. Tuttavia, la vostra dichiarazione dei dati non è ancora a prova di tipo — qualsiasi discrepanza tra la firma del metodo e i dati dichiarati emergerà a tempo di esecuzione quando “aggregando” gli argomenti. Non dimentichiamo che dichiarare la classe aggregatore aggiunge più codice di supporto necessario per far funzionare la vostra parametrizzazione. E abbiamo sempre preferito @CsvSource
rispetto a @EnumSource
per evitare il codice extra.
Documentabile
A differenza degli altri metodi, la sorgente enum ha simboli Java sia per gli insiemi di parametri (istanze enum) che per tutti i parametri che contengono (campi enum). Forniscono un luogo diretto dove collegare la documentazione nella sua forma più naturale — il JavaDoc.
Non è che la documentazione non possa essere posta altrove, ma sarà — per definizione — posta più lontano da ciò che descrive e quindi più difficile da trovare e più facile da rendere obsoleta.
Ma c’è di più!
Adesso: Enums. Sono. Classi.
Sembra che molti sviluppatori junior non abbiano ancora realizzato quanto siano potenti i enum Java.
Negli altri linguaggi di programmazione, sono davvero solo costanti migliorate. Ma in Java, sono comode piccole implementazioni di un pattern Flyweight design con (molta della) advantages delle classi complete.
Perché è una cosa buona?
Comportamento Relativo al Test Fixture
Comme tutte le altre classi, gli enum possono avere metodi aggiunti.
Questo diventa utile se i parametri di test enum vengono riutilizzati tra i test — stessi dati, solo testati leggermente diversamente. Per lavorare efficacemente con i parametri senza un significativo copia e incolla, è necessario condividere del codice helper tra quei test.
Non è qualcosa che una classe helper e qualche metodo statico non potrebbe “risolvere”.
- Nota a margine: Notare che tale progettazione soffre di un Feature Envy. I metodi di test – o peggio, i metodi della classe helper – dovrebbero estrarre i dati dagli oggetti enum per eseguire azioni su quei dati.
While questo è il (unico) modo in programmazione procedurale, nel mondo orientato agli oggetti possiamo fare di meglio.
Dichiarando i metodi “helper” direttamente nella dichiarazione dell’enum, sposteremmo il codice dove si trovano i dati. O, per usarla in linguaggio OOP, i metodi helper diventerebbero il “comportamento” delle fixture di test implementate come enum. Questo non solo renderebbe il codice più idiomatico (chiamare metodi sensibili sugli istanti rispetto ai metodi statici che passano dati), ma renderebbe anche più facile riutilizzare i parametri enum tra i casi di test.
Ereditarietà
Le enum possono implementare interfacce con metodi (predefiniti). Quando usati con senso, questo può essere sfruttato per condividere il comportamento tra diversi fornitori di dati – diverse enum.
Un esempio che viene facilmente in mente sono enum separati per test positivi e negativi. Se rappresentano un tipo simile di fixture di test, è probabile che abbianosome comportamento da condividere.
La Parola è Poco
Lasciamo illustrare questo su una suite di test di un ipotetico convertitore di file di codice sorgente, non troppo diverso da quello che esegue la conversione da Python 2 a 3.
Per avere vera fiducia in ciò che fa uno strumento così completo, si finirebbe con un esteso set di file di input che manifestano vari aspetti della lingua, e file corrispondenti per confrontare il risultato della conversione. Eccezion fatta per questo, è necessario verificare quali avvisi/errore vengono forniti all’utente per input problematici.
Questo è un’adattamento naturale per i test parametrizzati a causa del gran numero di campioni da verificare, ma non si adatta esattamente a nessuna delle semplici fonti di parametri JUnit, poiché i dati sono piuttosto complessi.
Vedi sotto:
enum Conversion {
CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
"Using module 'xyz' that is deprecated"
)),
SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
// Molti, molti altri ...
@Nonnull
final String inFile;
@CheckForNull
final String expectedOutput;
@CheckForNull
final Exception expectedException;
@Nonnull
final Set expectedWarnings;
Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set expectedWarnings) {
this(inFile, expectedOutput, null, expectedWarnings);
}
Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
this(inFile, null, expectedException, Set.of());
}
Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set expectedWarnings) {
this.inFile = inFile;
this.expectedOutput = expectedOutput;
this.expectedException = expectedException;
this.expectedWarnings = expectedWarnings;
}
public File getV2File() { ... }
public File getV3File() { ... }
}
@ParameterizedTest
@EnumSource
void upgrade(Conversion con) {
try {
File actual = convert(con.getV2File());
if (con.expectedException != null) {
fail("No exception thrown when one was expected", con.expectedException);
}
assertEquals(con.expectedWarnings, getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
} catch (Exception ex) {
assertTypeAndMessageEquals(con.expectedException, ex);
}
}
L’uso degli enum non ci limita nella complessità che possono avere i dati. Come puoi vedere, possiamo definire diversi costruttori convenienti negli enum, quindi dichiarare nuovi set di parametri è gradevole e pulito. Questo impedisce l’uso di elenchi di argomenti lunghi che spesso finiscono pieni di molti valori “vuoti” (null, stringhe vuote o raccolte) che lasciano uno a domandarsi cosa rappresenti effettivamente l’argomento n°7 — sai, uno dei null.
Notare come gli enum abilitino l’uso di tipi complessi (Set
, RuntimeException
) senza restrizioni o conversioni magiche. Passare tali dati è anche completamente sicuro.
ORA, so cosa stai pensando. Questo è molto verboso. Beh, fino a un certo punto. Realisticamente, avrai molti più campioni di dati da verificare, quindi la quantità di codice standard diventerà meno significativa in confronto.
Inoltre, vedi come i test correlati possano essere scritti sfruttando gli stessi enum e i loro metodi di aiuto:
@ParameterizedTest
@EnumSource
// Aggiornare file già aggiornati sempre passa, non fa nessuna modifica, non emette alcun avviso.
void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
}
@ParameterizedTest
@EnumSource
// Abbassare la versione dei file creati dalla procedura di aggiornamento è atteso che sempre passi senza avvisi.
void downgrade(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV2File());
}
Alcune altre discussioni Dopo Tutto
Concettualmente, @EnumSource
ti incoraggia a creare una descrizione complessa e leggibile da macchina di singoli scenari di test, sfocando la linea tra fornitori di dati e fixture di test.
Un’altra cosa fantastica dell’avere ciascun set di dati espresso come un simbolo Java (elemento enum) è che possono essere utilizzati singolarmente; completamente al di fuori dei fornitori di dati/test parametrizzati. Poiché hanno un nome ragionevole e sono autocontenuti (in termini di dati e comportamento), contribuiscono a test gradevoli e leggibili.
@Test
void warnWhenNoEventsReported() throws Exception {
FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
// read() è un metodo ausiliario condiviso da tutte le FixtureXmls
try (InputStream is = events.read()) {
EventList el = consume(is);
assertEquals(Set.of(...), el.getWarnings());
}
}
Adesso, @EnumSource
non sarà una delle tue fonti di argomenti più utilizzate, ed è un bene, poiché utilizzarla troppo potrebbe non essere utile. Ma nelle giuste circostanze, è utile sapere come utilizzare tutte le loro offerte.
Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource