JUnit 5 per gli sviluppatori Kotlin

1. Introduzione

JUnit 5 appena rilasciato è la prossima versione del noto framework di test per Java. Questa versione include una serie di funzionalità che mirano specificamente alle funzionalità introdotte in Java 8 : è basata principalmente sull'uso di espressioni lambda.

In questo rapido articolo, mostreremo come funziona lo stesso strumento con il linguaggio Kotlin .

2. Test JUnit 5 semplici

Nella sua forma più semplice, un test JUnit 5 scritto in Kotlin funziona esattamente come ci si aspetterebbe. Scriviamo una classe di test, annotiamo i nostri metodi di test con l' annotazione @Test , scriviamo il nostro codice ed eseguiamo le asserzioni:

class CalculatorTest { private val calculator = Calculator() @Test fun whenAdding1and3_thenAnswerIs4() { Assertions.assertEquals(4, calculator.add(1, 3)) } }

Tutto qui funziona appena fuori dagli schemi. Possiamo utilizzare le annotazioni standard @Test, @BeforeAll, @BeforeEach, @AfterEach e @AfterAll . Possiamo anche interagire con i campi nella classe di test esattamente come in Java.

Nota che le importazioni richieste sono diverse e facciamo asserzioni utilizzando la classe Assertions invece della classe Assert . Questa è una modifica standard per JUnit 5 e non è specifica per Kotlin.

Prima di andare oltre, cambiamo il nome del test e usiamo gli identificatori b acktick in Kotlin:

@Test fun `Adding 1 and 3 should be equal to 4`() { Assertions.assertEquals(4, calculator.add(1, 3)) }

Ora è molto più leggibile! In Kotlin, possiamo dichiarare tutte le variabili e le funzioni usando i backtick, ma non è consigliabile farlo per i normali casi d'uso.

3. Asserzioni avanzate

JUnit 5 aggiunge alcune asserzioni avanzate per lavorare con lambda . Funzionano allo stesso modo in Kotlin come in Java, ma devono essere espressi in un modo leggermente diverso a causa del modo in cui funziona il linguaggio.

3.1. Affermare le eccezioni

JUnit 5 aggiunge un'asserzione per quando una chiamata dovrebbe generare un'eccezione. Possiamo verificare che una chiamata specifica, anziché una qualsiasi chiamata nel metodo, generi l'eccezione attesa. Possiamo anche affermare sull'eccezione stessa.

In Java, passeremmo un lambda alla chiamata a Assertions.assertThrows . Facciamo lo stesso in Kotlin, ma possiamo rendere il codice più leggibile aggiungendo un blocco alla fine della chiamata di asserzione:

@Test fun `Dividing by zero should throw the DivideByZeroException`() { val exception = Assertions.assertThrows(DivideByZeroException::class.java) { calculator.divide(5, 0) } Assertions.assertEquals(5, exception.numerator) }

Questo codice funziona esattamente come l'equivalente Java ma è più facile da leggere , poiché non è necessario passare un lambda all'interno delle parentesi dove chiamiamo la funzione assertThrows .

3.2. Asserzioni multiple

JUnit 5 aggiunge la possibilità di eseguire più asserzioni contemporaneamente e le valuterà tutte e segnalerà tutti gli errori.

Questo ci consente di raccogliere più informazioni in una singola esecuzione di test piuttosto che essere costretti a correggere un errore solo per raggiungere quello successivo. Per fare ciò, chiamiamo Assertions.assertAll , passando un numero arbitrario di lambda.

In Kotlin , dobbiamo gestirlo in modo leggermente diverso. La funzione in realtà accetta un parametro varargs di tipo eseguibile .

Al momento, non c'è supporto per il cast automatico di un lambda su un'interfaccia funzionale, quindi dobbiamo farlo a mano:

fun `The square of a number should be equal to that number multiplied in itself`() { Assertions.assertAll( Executable { Assertions.assertEquals(1, calculator.square(1)) }, Executable { Assertions.assertEquals(4, calculator.square(2)) }, Executable { Assertions.assertEquals(9, calculator.square(3)) } ) }

3.3. Fornitori di test veri e falsi

A volte, vogliamo verificare che alcune chiamate restituiscano un valore vero o falso . Storicamente dovremmo calcolare questo valore e chiamare assertTrue o assertFalse come appropriato. JUnit 5 consente di fornire un lambda invece che restituisce il valore controllato.

Kotlin ci consente di passare un lambda nello stesso modo in cui abbiamo visto sopra per testare le eccezioni. Possiamo anche passare riferimenti al metodo . Ciò è particolarmente utile quando si verifica il valore di ritorno di alcuni oggetti esistenti come facciamo qui usando List.isEmpty :

@Test fun `isEmpty should return true for empty lists`() { val list = listOf() Assertions.assertTrue(list::isEmpty) }

3.4. Fornitori per messaggi di errore

In alcuni casi, desideriamo fornire il nostro messaggio di errore da visualizzare in caso di errore di asserzione invece di quello predefinito.

Spesso si tratta di stringhe semplici, ma a volte potremmo voler utilizzare una stringa costosa da calcolare . In JUnit 5, possiamo fornire un lambda per calcolare questa stringa e viene chiamato solo in caso di errore invece di essere calcolato in anticipo.

Questo può aiutare a rendere i test più veloci e ridurre i tempi di costruzione . Funziona esattamente come abbiamo visto prima:

@Test fun `3 is equal to 4`() { Assertions.assertEquals(3, 4) { "Three does not equal four" } }

4. Test basati sui dati

Uno dei grandi miglioramenti in JUnit 5 è il supporto nativo per i test basati sui dati . Questi funzionano altrettanto bene in Kotlin e l'uso di mappature funzionali sulle raccolte può rendere i nostri test più facili da leggere e mantenere.

4.1. Metodi TestFactory

Il modo più semplice per gestire i test basati sui dati è utilizzare l' annotazione @TestFactory . Questo sostituisce l' annotazione @Test e il metodo restituisce una raccolta di istanze di DynamicNode , normalmente create chiamando DynamicTest.dynamicTest .

Funziona esattamente allo stesso modo in Kotlin e possiamo passare di nuovo lambda in modo più pulito , come abbiamo visto prima:

@TestFactory fun testSquares() = listOf( DynamicTest.dynamicTest("when I calculate 1^2 then I get 1") { Assertions.assertEquals(1,calculator.square(1))}, DynamicTest.dynamicTest("when I calculate 2^2 then I get 4") { Assertions.assertEquals(4,calculator.square(2))}, DynamicTest.dynamicTest("when I calculate 3^2 then I get 9") { Assertions.assertEquals(9,calculator.square(3))} )

Possiamo fare di meglio però. Possiamo facilmente costruire il nostro elenco eseguendo alcune mappature funzionali su un semplice elenco di dati di input:

@TestFactory fun testSquares() = listOf( 1 to 1, 2 to 4, 3 to 9, 4 to 16, 5 to 25) .map { (input, expected) -> DynamicTest.dynamicTest("when I calculate $input^2 then I get $expected") { Assertions.assertEquals(expected, calculator.square(input)) } }

Immediatamente, possiamo aggiungere facilmente più casi di test all'elenco di input e verranno aggiunti automaticamente i test.

We can also create the input list as a class field and share it between multiple tests:

private val squaresTestData = listOf( 1 to 1, 2 to 4, 3 to 9, 4 to 16, 5 to 25) 
@TestFactory fun testSquares() = squaresTestData .map { (input, expected) -> DynamicTest.dynamicTest("when I calculate $input^2 then I get $expected") { Assertions.assertEquals(expected, calculator.square(input)) } }
@TestFactory fun testSquareRoots() = squaresTestData .map { (expected, input) -> DynamicTest.dynamicTest("when I calculate the square root of $input then I get $expected") { Assertions.assertEquals(expected, calculator.squareRoot(input)) } }

4.2. Parameterized Tests

There are experimental extensions to JUnit 5 to allow easier ways to write parameterized tests. These are done using the @ParameterizedTest annotation from the org.junit.jupiter:junit-jupiter-params dependency:

 org.junit.jupiter junit-jupiter-params 5.0.0 

The latest version can be found on Maven Central.

The @MethodSource annotation allows us to produce test parameters by calling a static function that resides in the same class as the test. This is possible but not obvious in Kotlin. We have to use the @JvmStatic annotation inside a companion object:

@ParameterizedTest @MethodSource("squares") fun testSquares(input: Int, expected: Int) { Assertions.assertEquals(expected, input * input) } companion object { @JvmStatic fun squares() = listOf( Arguments.of(1, 1), Arguments.of(2, 4) ) }

This also means that the methods used to produce parameters must all be together since we can only have a single companion object per class.

All of the other ways of using parameterized tests work exactly the same in Kotlin as they do in Java. @CsvSource is of special note here, since we can use that instead of @MethodSource for simple test data most of the time to make our tests more readable:

@ParameterizedTest @CsvSource( "1, 1", "2, 4", "3, 9" ) fun testSquares(input: Int, expected: Int) { Assertions.assertEquals(expected, input * input) }

5. Tagged Tests

The Kotlin language does not currently allow for repeated annotations on classes and methods. This makes the use of tags slightly more verbose, as we are required to wrap them in the @Tags annotation:

@Tags( Tag("slow"), Tag("logarithms") ) @Test fun `Log to base 2 of 8 should be equal to 3`() { Assertions.assertEquals(3.0, calculator.log(2, 8)) }

Questo è richiesto anche in Java 7 ed è già completamente supportato da JUnit 5.

6. Riepilogo

JUnit 5 aggiunge alcuni potenti strumenti di test che possiamo usare. Questi funzionano quasi tutti perfettamente con il linguaggio Kotlin, sebbene in alcuni casi con una sintassi leggermente diversa da quella che vediamo negli equivalenti Java.

Spesso, però, questi cambiamenti nella sintassi sono più facili da leggere e lavorare con quando si usa Kotlin.

Esempi di tutte queste funzionalità possono essere trovati su GitHub.