Utiliser un framework de test en Kotlin
-
Sommaire
Introduction
Dans ce tutoriel, je vais tout d’abord vous montrer une utilisation de framework de test, ce qu’il est possible de faire et indiquer pourquoi c’est intéressant. Dans un second temps, j’expliquerai comment installer le framework de test dans l’environnement de travail de Forge.
Les explications se font en Kotlin, avec un framework axé sur Kotlin, cependant, le même principe existe en Java, avec une syntaxe différente.
Pré-requis
- Être curieux,
- Avoir déjà une expérience de programmation,
- En Kotlin si possible, mais ce n’est pas indispensable.
Les Tests Unitaires
Introduction aux tests
Les Tests Unitaires sont des programmes chargés de tester d’autres programmes, ces derniers formant le programme principale. Par exemple, si vous aviez à écrire une fonction qui renvoie la somme de deux entiers passés en paramètres, les tests unitaires correspondants pourraient vérifier que la somme est correcte sur quelques exemples, entre autre avec l’un des paramètres à 0, ou avec des entiers négatifs.
L’exemple est contraint, mais l’idée est là.
Exemple avec Vec3i
Et mon vrai exemple n’est pas si éloigné. Je voudrais pouvoir écrire ceci en Kotlin :
val a = Vec3i(1, 2, 3) val b = Vec3i(3, 2, 1) val c= a + b
Rappels Kotlin :
- les appels à Vec3i sont des appels aux constructeurs ; le new est implicite en Kotlin,
- val indique une valeur non mutable du type à droite de l’assignation, ici Vec3i,
- il n’y a pas de point-virgule à la fin des lignes.
Autrement dit, je voudrais pouvoir additionner des instances de vecteurs en utilisation une notation naturelle. Pour cela, je vais me reposer sur les tests et les outils d’aide de IntelliJ IDEA (Eclipse en a de similaires).
J’écris donc mon fichier de tests suivant une des syntaxes supportés par KotlinTest et je décris ce que je veux obtenir dans un fichier VectorExtensionsTest.kt:
package org.puupuu.kotlinbasedmod import io.kotlintest.specs.WordSpec import net.minecraft.util.math.Vec3i class VectorExtensionsTest : WordSpec() { init { "Vec3i" should { "respond to add operator" { val a = Vec3i(1, 2, 3) val b = Vec3i(3, 2, 1) a + b shouldBe Vec3i(4, 4, 4) } } } }
Notes sur l’enrobage:
- le nom du fichier VectorExtensionsTest.kt est basé sur le nom probable du fichier source qui contiendra le code, VectorExtensions.kt,
- le test se situe dans le même package que le code source. Ce n’est pas obligatoire, cela va cependant faciliter l’écriture dans le cas présent,
- l’import de WordSpec indique quelle syntaxe de test je veux utiliser. KotlinTest supporte différentes syntaxes d’écriture de tests ; cela sort du domaine de ce tutoriel,
- l’import de Vec3i, bien entendu, me permet de construire des objets de ce type,
- le clause init{} dans une classe Kotlin est un constructeur, ici sans paramètre. C’est ici que les tests se placent dans KotlinTest.
Ensuite vient la description du test proprement dit :
- une chaîne de caractères indique le sujet du test. Ici, je teste un comportement de Vec3i, c’est ce que j’indique,
- should est une fonction d’extension infixe du framework de test. Si ce début de phrase vous est incompréhensible, ce n’est pas grave pour la compréhension, vous pouvez prendre ça pour un mot-clé. Sinon, voyez plus loin dans le Bonus,
- suit une fonction anonyme entre accolades contenant une suite de tests,
- les tests sont ensuite sur le modèle : chaîne de caractères indiquant un contrat de test suivi d’une fonction anonyme implémentant le test.
Les chaînes de caractères sont purement destinées au lecteur humain. L’idée, avec cette syntaxe, est de pouvoir lire le contrat que le test veut vérifier : « Vec3i should respond to add operator », « Vec3i devrait supporter l’opérateur d’addition ».
Ce code ne compile pas, bien évidemment, puisque Vec3i ne supporte pas l’opérateur d’addition. On entre ici dans le domaine du TDD (Test Driven Developement / Développement Dirigé par les Tests). J’ai d’abord écrit un test avec du code tel que j’aimerais l’écrire, mais qui ne fonctionne pas.
Reste donc à l’implémenter. Grâce aux outils d’aide de l’IDE, c’est assez facile : je me place sur l’erreur détectée, ici l’utilisation du ‘+’ et je demande l’aide à la correction d’erreur (Alt-Enter sur IntelliJ IDEA). L’IDE me propose comme premier choix de créer une fonction d’extension pour l’opérateur ‘+’. Parfait. J’accepte.
Je réponds ensuite aux question de l’IDE en choisissant les bons types de paramètre et de valeur de retour, et j’obtiens ceci :
infix operator fun Vec3i.plus(vec3i: Vec3i): Vec3i { throw UnsupportedOperationException("not implemented") //To change body of created functions use File | Settings | File Templates. }
La manipulation dure à peu près 5 secondes quand on est habitué. Autant dire que c’est très pratique.
À présent, le code compile, mais vous l’imaginez bien, vu le corps de la fonction, on ne va pas aller loin. Cela vaut la peine d’essayer tout de même. Une petite flèche verte est apparue au niveau de ma classe de test qui m’invite à l’exécuter.
On arrive ici à un des grands intérêts de cet ensemble d’outils : il est inutile de lancer Minecraft au complet pour tester un morceau de code. On peut itérer sur une petite partie, s’assurer que cela fonctionne comme on veut, puis l’intégrer dans une partie plus large et tester à la main en lançant le jeu complet.
Cela évite de lancer le jeu, d’attendre un certain temps, pour s’apercevoir d’une erreur toute bête…
Comme prévu, le lancement se passe assez mal :
Dans la philosophie du TDD, cette étape est importante : en validant que le test échoue lorsque le code ne fait pas ce qu’il faut, on valide que lorsqu’une erreur est présente, elle est bien détectée par le framework de tests.
Je peux donc maintenant implémenter mon opérateur (en changeant au passage le nom du paramètre qui était assez mal choisi) :
infix operator fun Vec3i.plus(other: Vec3i): Vec3i { return Vec3i(this.x + other.x, this.y + other.y, this.z + other.z) }
Je peux relancer les tests. Et c’est vert !
Emporté par mon élan, je peux implémenter l’opérateur de soustraction. En 30 secondes, j’ai mon nouveau test et ma nouvelle implémentation, un beau copier coller de l’opérateur d’addition. C’est fait exprès, cela me permet de provoquer un échec, et de vous montrer le rapport d’erreur, qui indique la valeur qui était attendue, et la valeur reçue.
La correction est triviale, et les tests m’indiquent que je ne me suis pas trompé :
infix operator fun Vec3i.minus(other: Vec3i): Vec3i { return Vec3i(this.x - other.x, this.y - other.y, this.z - other.z) }
Reste une dernière opération : les fonctions minus et plus ne peuvent pas rester dans le fichier de tests, d’où elles ne pourraient pas être utiliser par le programme principal. J’utilise l’aide à la programmation de l’IDE pour déplacer la fonction automatiquement dans un fichier du package côté sources (F6 avec IntelliJ IDEA).
L’IDE se charge de déplacer le code avec les import nécessaires. Pas d’erreur possible du à un copier/coller raté.
Je relance une fois les tests pour vérifier que tout fonctionne toujours et que je n’ai pas fait d’erreur dans le déplacement, changer une accessibilité,… Tout va bien.
Et le contenu des deux fichiers à cette révision-là:
Configuration de KotlinTest
À présent que vous avez vu une déroulement classique de l’utilisation d’un framework de test, voyons comment configurer celui que j’utilise.
Le framework de test que j’utilise pour Kotlin s’appelle KotlinTest. Il est dédié à Kotlin et se sert de ses facilités syntaxiques. Il existe d’autres framework de tests en Java. Le plus connu est JUnit.
Voilà comment je modifie mon fichier build.gradle.
Tout d’abord, j’indique la localisation des fichiers de tests, à côté des fichiers sources principaux. Ce n’est pas nécessaire si vos fichiers en Kotlin sont dans des répertoires suivant la nomenclature Java, Gradle sait où les chercher. Mais même avec le plugin Kotlin, il semblerait qu’il faille aider Gradle si vous suivez une nomenclature Kotlin :
sourceSets { main.kotlin.srcDirs += 'src/main/kotlin' test.kotlin.srcDirs += 'src/test/kotlin' }
Puis dans la section dependencies, j’ajoute les dépendances de test en utilisant testCompile. Cette dépendance n’aura pas d’influence sur le build principal, mais uniquement sur un build de test. KotlinTest est disponible dans Maven Central. Votre fichier est probablement déjà configuré pour aller chercher le package automatiquement.
dependencies { // [… vos dépendances ...] testCompile 'io.kotlintest:kotlintest:1.3.1' }
Dans l’arborescence des sources, j’ajoute le répertoire pour les sources des tests:
├── main │ ├── kotlin │ │ └── org […] │ └── resources │ ├── assets […] └── test └── kotlin
Il est important de le faire avant la régénération de l’environnement de travail, en tout cas pour IntelliJ IDEA, afin que ce répertoire soit bien pris comme un package. Au pire, si vous l’avez oublié, vous pouvez le créer plus tard et régénérer l’environnement de travail pour votre IDE. Dans mon cas :
./gradlew idea
Dans le cas de IntelliJ IDEA, l’IDE va aussi vous indiquer que le package n’est pas configuré pour Kotlin, et vous demandez si vous voulez le faire. Répondez que oui, et l’IDE se chargera du reste.
Enfin, dans ce nouveau répertoire test/kotlin, je recréé l’arborescence de mon package sur le modèle du répertoire src/test. J’ajoute ensuite dans la hiérarchie test/kotlin mes fichiers de tests, dont les noms sont suffixés par Test, et j’ajoute dans la hiérarchie src/kotlin les fichiers d’implémentation.
Bonus
« Fonction d’extension infixe du framework de test »
Les fonctions d’extension de Kotlin, comme dans les autres langages les proposant, sont des fonctions qui peuvent être appelées comme si elles étaient des membres d’une classe, alors qu’elles ne le sont pas. Elles étendent la classe.
Ici, should étend la classe String.
Une fonction infixe est une fonction à deux paramètres (dont le premier est le receveur du message qui deviendra le this de la méthode) qui peut s’écrire comme une opération mathématique, avec : le premier paramètre, puis le nom de la fonction et enfin le second paramètre. Le compilateur comprendre la syntaxe a func b comme étant a.func(b).
Ici, “Vec3i” should { … } est compris comme “Vec3i”.should({…})
C’est sur une manière similaire qu’est implémentée la fonction shouldBe.
Crédits
Rédaction :
- Mokona78
Ce tutoriel de Minecraft Forge France est mis à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International -
Sachant que Kotlin se compile en JVM, peut-on utiliser des frameworks de test en java comme junit par exemple ?
-
Hello,
oui, on peut utiliser un framework Java, comme junit, pour tester du Kotlin.
Cependant, il y a certaines constructions Kotlin qui produisent du code JVM dont il faut connaître la nomenclature. C’est donc faisable, et dans les grandes lignes utilisables, avec peut-être quelques cas un peu moins agréables à écrire.