JUnit 5 pour les développeurs Kotlin

1. Introduction

Le JUnit 5 nouvellement publié est la prochaine version du cadre de test bien connu pour Java. Cette version comprend un certain nombre de fonctionnalités qui ciblent spécifiquement les fonctionnalités introduites dans Java 8 - elle est principalement construite autour de l'utilisation d'expressions lambda.

Dans cet article rapide, nous montrerons à quel point le même outil fonctionne avec le langage Kotlin .

2. Tests JUnit 5 simples

Dans sa forme la plus simple, un test JUnit 5 écrit en Kotlin fonctionne exactement comme prévu. Nous écrivons une classe de test, annotons nos méthodes de test avec l' annotation @Test , écrivons notre code et effectuons les assertions:

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

Tout ici fonctionne hors de la boîte. Nous pouvons utiliser les annotations standard @Test, @BeforeAll, @BeforeEach, @AfterEach et @AfterAll . Nous pouvons également interagir avec les champs de la classe de test exactement de la même manière qu'en Java.

Notez que les importations requises sont différentes et que nous faisons des assertions en utilisant la classe Assertions au lieu de la classe Assert . Il s'agit d'un changement standard pour JUnit 5 et n'est pas spécifique à Kotlin.

Avant d'aller plus loin, changeons le nom du test et utilisons les identifiants b acktick dans Kotlin:

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

Maintenant, c'est beaucoup plus lisible! Dans Kotlin, nous pouvons déclarer toutes les variables et fonctions en utilisant des backticks, mais il n'est pas recommandé de le faire pour les cas d'utilisation normaux.

3. Assertions avancées

JUnit 5 ajoute des assertions avancées pour travailler avec des lambdas . Celles-ci fonctionnent de la même manière en Kotlin qu'en Java, mais doivent être exprimées d'une manière légèrement différente en raison de la façon dont le langage fonctionne.

3.1. Faire valoir des exceptions

JUnit 5 ajoute une assertion pour le moment où un appel est censé lever une exception. Nous pouvons tester qu'un appel spécifique - plutôt que n'importe quel appel de la méthode - lève l'exception attendue. On peut même affirmer sur l'exception elle-même.

En Java, nous passions un lambda dans l'appel à Assertions.assertThrows . Nous faisons la même chose dans Kotlin, mais nous pouvons rendre le code plus lisible en ajoutant un bloc à la fin de l'appel d'assertion:

@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) }

Ce code fonctionne exactement de la même manière que l'équivalent Java mais est plus facile à lire , car nous n'avons pas besoin de passer un lambda à l'intérieur des crochets où nous appelons la fonction assertThrows .

3.2. Assertions multiples

JUnit 5 ajoute la possibilité d' effectuer plusieurs assertions en même temps , et il les évaluera toutes et rendra compte de tous les échecs.

Cela nous permet de rassembler plus d'informations en un seul test plutôt que d'être obligé de corriger une erreur uniquement pour frapper la suivante. Pour ce faire, nous appelons Assertions.assertAll , en passant un nombre arbitraire de lambdas.

À Kotlin , nous devons gérer cela légèrement différemment. La fonction prend en fait un paramètre varargs de type Executable .

À l'heure actuelle, il n'y a pas de support pour le cast automatique d'un lambda vers une interface fonctionnelle, nous devons donc le faire à la main:

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. Fournisseurs pour les tests vrais et faux

À l'occasion, nous voulons tester qu'un appel renvoie une valeur vraie ou fausse . Historiquement, nous calculions cette valeur et appelions assertTrue ou assertFalse selon le cas. JUnit 5 permet de fournir un lambda à la place qui renvoie la valeur en cours de vérification.

Kotlin nous permet de passer dans un lambda de la même manière que nous l'avons vu ci-dessus pour tester les exceptions. Nous pouvons également passer des références de méthodes . Ceci est particulièrement utile lors du test de la valeur de retour d'un objet existant comme nous le faisons ici en utilisant List.isEmpty :

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

3.4. Fournisseurs pour les messages d'échec

Dans certains cas, nous souhaitons fournir notre propre message d'erreur à afficher en cas d'échec d'assertion au lieu de celui par défaut.

Il s'agit souvent de chaînes simples, mais parfois nous souhaitons utiliser une chaîne coûteuse à calculer . Dans JUnit 5, nous pouvons fournir un lambda pour calculer cette chaîne , et elle n'est appelée qu'en cas d'échec au lieu d'être calculée à l'avance.

Cela peut accélérer l'exécution des tests et réduire les temps de génération . Cela fonctionne exactement comme nous l'avons vu précédemment:

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

4. Tests basés sur les données

L'une des grandes améliorations de JUnit 5 est la prise en charge native des tests basés sur les données . Ceux-ci fonctionnent également bien dans Kotlin , et l'utilisation de mappages fonctionnels sur les collections peut rendre nos tests plus faciles à lire et à maintenir.

4.1. Méthodes TestFactory

Le moyen le plus simple de gérer les tests pilotés par les données consiste à utiliser l' annotation @TestFactory . Cela remplace l' annotation @Test et la méthode retourne une collection d' instances DynamicNode - normalement créées en appelant DynamicTest.dynamicTest .

Cela fonctionne exactement de la même manière dans Kotlin, et nous pouvons à nouveau passer dans le lambda de manière plus propre , comme nous l'avons vu précédemment:

@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))} )

Nous pouvons faire mieux que cela. Nous pouvons facilement construire notre liste en effectuant un mappage fonctionnel sur une simple liste d'entrée de données:

@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)) } }

Tout de suite, nous pouvons facilement ajouter plus de cas de test à la liste d'entrée, et cela ajoutera automatiquement des tests.

Nous pouvons également créer la liste d'entrée en tant que champ de classe et la partager entre plusieurs 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. Tests paramétrés

Il existe des extensions expérimentales de JUnit 5 pour permettre des moyens plus simples d'écrire des tests paramétrés. Celles-ci sont effectuées à l'aide de l' annotation @ParameterizedTest de la dépendance org.junit.jupiter: junit-jupiter-params :

 org.junit.jupiter junit-jupiter-params 5.0.0 

La dernière version est disponible sur Maven Central.

L' annotation @MethodSource nous permet de produire des paramètres de test en appelant une fonction statique qui réside dans la même classe que le test. C'est possible mais pas évident à Kotlin . Nous devons utiliser l' annotation @JvmStatic dans un objet compagnon:

@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) ) }

Cela signifie également que les méthodes utilisées pour produire les paramètres doivent toutes être ensemble puisque nous ne pouvons avoir qu'un seul objet compagnon par classe .

Toutes les autres façons d'utiliser les tests paramétrés fonctionnent exactement de la même manière dans Kotlin qu'en Java. @CsvSource est à noter ici, car nous pouvons l'utiliser à la place de @MethodSource pour des données de test simples la plupart du temps afin de rendre nos tests plus lisibles:

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

5. Tests marqués

Le langage Kotlin ne permet actuellement pas d'annotations répétées sur les classes et les méthodes. Cela rend l'utilisation des balises légèrement plus verbeuse, car nous devons les envelopper dans l' annotation @Tags :

@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)) }

Ceci est également requis dans Java 7 et est déjà entièrement pris en charge par JUnit 5.

6. Résumé

JUnit 5 ajoute des outils de test puissants que nous pouvons utiliser. Ceux-ci fonctionnent presque tous parfaitement avec le langage Kotlin, bien que dans certains cas avec une syntaxe légèrement différente de celle que nous voyons dans les équivalents Java.

Cependant, ces changements de syntaxe sont souvent plus faciles à lire et à utiliser lors de l'utilisation de Kotlin.

Des exemples de toutes ces fonctionnalités peuvent être trouvés sur GitHub.