JUnit 5 pour les développeurs Kotlin

JUnit 5 pour les développeurs Kotlin

1. introduction

Le nouveauJUnit 5 est la prochaine version du cadre de test bien connu pour Java. Cette version comprend un certain nombre defeatures that specifically target functionality introduced in Java 8 - elle est principalement construite autour de l'utilisation d'expressions lambda.

Dans cet article rapide, nous allons montrer à quel point le même outilworks with the Kotlin language.

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 de la même manière qu'en Java.

Notez que les importations requises sont différentes, etwe doassertions using the Assertions class instead of the Assert class. Il s’agit d’un changement standard pour JUnit 5 et n’est pas spécifique à Kotlin.

Avant d'aller plus loin, modifions le nom du test et utilisonsbacktick identifiers 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 à l’aide d’accompagnement, mais il n’est pas recommandé de le faire pour les cas d’utilisation normaux.

3. Assertions avancées

JUnit 5 adds some advanced assertions forworking with lambdas. Celles-ci fonctionnent de la même manière en Kotlin et en Java, mais doivent être exprimées de manière légèrement différente en raison du fonctionnement du langage.

3.1. Faire valoir des exceptions

JUnit 5 ajoute une assertion pour quand un appel est censé générer une exception. We can test that a specific call — rather than just any call in the method — throws the expected exception. On peut même affirmer sur l'exception elle-même.

En Java, nous passerions un lambda dans l'appel àAssertions.assertThrows. Nous faisons la même chose dans Kotlin, mais nous pouvonsmake the code more readable 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 codeworks exactly the same as the Java equivalent but is easier to read, car nous n'avons pas besoin de passer un lambda à l'intérieur des crochets où nous appelons la fonctionassertThrows.

3.2. Assertions multiples

JUnit 5 ajoute la possibilité auxperform multiple assertions at the same time, et il les évaluera tous et rendra compte de tous les échecs.

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

In Kotlin, nous devons gérer cela légèrement différemment. Lesfunction actually takes a varargs parameter of type Executable.

À l'heure actuelle, il n'y a pas de support pour la conversion 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 valeurtrue oufalse. Historiquement, nous calculions cette valeur et appelionsassertTrue ouassertFalse selon le cas. JUnit 5 permet de fournir un lambda qui renvoie la valeur vérifiée.

Kotlin allows us to pass in a lambda de la même manière que nous l'avons vu ci-dessus pour tester les exceptions. We can also pass in method references. Ceci est particulièrement utile lors du test de la valeur de retour d'un objet existant comme nous le faisons ici en utilisantList.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 voulons queprovide our own error message soit affiché sur un échec d'assertion au lieu de celui par défaut.

Ce sont souvent de simples chaînes, maissometimes we may want to use a string that is expensive to compute. Dans JUnit 5, nous pouvonsprovide a lambda to compute this string, et c'estonly called on failure au lieu d'être calculé à l'avance.

Cela peut aidermake the tests run faster and reduce build times. Cela fonctionne exactement comme nous l'avons vu auparavant:

@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 lenative support for data-driven tests. Ceswork equally well in Kotlin et l'utilisation de mappages fonctionnels sur les collections peuventmake our tests easier to read and maintain.

4.1. Méthodes TestFactory

Le moyen le plus simple de gérer les tests basés sur les données consiste à utiliser l'annotation@TestFactory. Cela remplace l'annotation@Test et la méthode retourne une collection d'instancesDynamicNode - normalement créées en appelantDynamicTest.dynamicTest.

Cela fonctionne exactement de la même manière dans Kotlin, et nous pouvons à nouveaupass in the lambda in a cleaner way, 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 cependant. On peut facilementbuild our list by performing some functional mapping on a simple input list of data:

@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 scénarios de test à la liste de saisie et ajouter automatiquement des tests.

On peut aussicreate the input list as a class field et le 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 desexperimental extensions to 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épendanceorg.junit.jupiter:junit-jupiter-params:


    org.junit.jupiter
    junit-jupiter-params
    5.0.0

La dernière version peut être trouvée surMaven Central.

L'annotation@MethodSource nous permet deproduce test parameters by calling a static function qui réside dans la même classe que le test. Ceci est possible maisnot obvious in Kotlin. Nous devonsuse 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)
    )
}

Cela signifie également que les méthodes utilisées pour produire les paramètres doivent toutes être ensemble depuiswe can only have a single companion object per class.

Tous les autres moyens d’utiliser des tests paramétrés fonctionnent exactement de la même manière en Kotlin et 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 pour 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 Kotlindoes not currently allow for repeated annotations sur les classes et les méthodes. Cela rend l'utilisation des balises légèrement plus verbeuse, car nous sommes obligés dewrap 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))
}

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

6. Sommaire

JUnit 5 ajoute de puissants outils de test que nous pouvons utiliser. Celles-ci fonctionnent presque toutes parfaitement avec le langage Kotlin, bien que dans certains cas, la syntaxe soit légèrement différente de celle des é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ésover on GitHub.