Mon premier plugin Gradle — La Suite

Olivier Gauthier
BeTomorrow
Published in
7 min readJan 23, 2018

Vous avez suivi le précédent article et vous avez commencé à développer votre plugin Gradle mais l’idée de le déboguer au println ne vous enchante guère. Comme je vous comprends ! 😄 Nous allons donc voir comment tester et déboguer. On verra aussi comment organiser son code quand le plugin commence à grossir.

Pour commencer, je vous propose de repartir de là où on s’était arrêté en clonant le dépôt suivant :

git clone git@github.com:oliviergauthier/my-first-gradle-plugin.git

Pour rappel, c’est un plugin qui ajoute une tâche permettant de générer un fichier de métadonnées à la racine du projet. Ces métadonnées contiennent différentes informations comme le numéro de version, la date, le hash de commit et les 10 derniers logs git.

Paramétrage

Avant de commencer on va ajouter les dépendances de tests dans notre build.gradle.

Ici nous avons ajouté deux dépendances : Spock, un framework de test en Groovy qui va nous faciliter la vie, et gradleTestKit qui nous permettra de faire des tests d’intégration plus tard.

Tests Unitaires

On va commencer par vérifier que le plugin génère bien une tâche info lorsqu’on l’applique. Nous allons donc ajouter la classe MyFirstPluginTest dans le dossier de tests (n’oubliez pas de mettre le même package que la classe testée soit dans mon cas com.betomorrow.gradle.sample)

Un petit mot sur Spock avant de continuer. Spock est un framework de test incontournable en Groovy qui va permettre de mieux structurer nos tests et de facilement faire des mocks comme nous aurions pu le faire en Java avec JUnit5 + Mockito. Une classe de test doit forcément étendre la classe Specification puis définir des “feature methods” dans lesquelles nous aurons des blocks given/when/then ou encore expect/where. Je vous invite à parcourir rapidement leur documentation ici pour plus de détails.

Nous avons donc défini une classe MyFirstPluginTest qui étend Specification et une “feature method” nommée “test apply creates info task”. Dans le block when nous utilisons le ProjectBuilder fourni par Gradle pour créer un projet vide. En effet nous ne pouvons pas directement faire new Project() ou new Task(), ces objets ont des dépendances internes et il faudra forcément passer par ProjectBuilder pour les construire. Nous appliquons ensuite notre plugin via son identifiant et nous validons que la tâche info est bien présente dans le block then. Vous pouvez lancer maintenant la tâche ‘gradle test’ ou bien exécuter la méthode de test directement depuis votre IDE ce qui vous permettra en plus de pouvoir utiliser le débogueur.

Tests Fonctionnels

Nous avons vu comment créer un test unitaire avec ProjetBuilder, maintenant voyons comment créer un test fonctionnel avec Gradle Test Kit. Avec un test fonctionnel, vous allez pouvoir tester votre plugin en conditions réelles, récupérer sa sortie et vérifier qu’il s’est comporté comme attendu. Au passage, vous allez même pouvoir utiliser votre débogueur et suivre pas à pas ce qu’il se passe.

Commençons par un cas simple. Nous allons écrire un test qui définit une tâche HelloWorld et qui l’exécute. Pour cela on ajoute la classe MyFirstFunctionalTest dans src/test/groovy.

Ceci est un test extrait de la documentation officielle. Dans la partie setup on crée un fichier temporaire build.gradle dont on va définir le contenu dans la partie given du test. Dans la partie when nous exécutons Gradle à l’aide de GradleRunner en spécifiant le répertoire d’exécution et la tâche à appeler. Enfin dans la partie then on fait quelques assertions.

Voilà ça c’était le cas facile où la tâche est définie dans le test lui-même. Si maintenant on essaie de remplacer le contenu du buildFile par

plugins {
id 'com.betomorrow.my-first-gradle-plugin'
}

version = "1.0-SNAPSHOT"

info {
filename = "package.json"
logSize = 1
}

Et que l’on appelle la tâche info nous allons avoir l’erreur suivante

Plugin [id: 'com.betomorrow.my-first-gradle-plugin'] was not found in any of the following sources:

En effet, le runner gradle n’exécute pas Gradle avec le même classpath que notre plugin. On va donc devoir faire quelques modifications pour pouvoir dire à GradleRunner où trouver notre plugin. Cela consiste à générer un fichier texte contenant la liste des classes à inclure et donner ce fichier à GradleRunner (cf doc officielle). Fort heureusement, Gradle fournit maintenant tout ce qu’il faut pour nous faciliter le travail.

Nous commençons donc par ajouter le plugin java-gradle-plugin dans notre système de build (c’est l’info à ne pas manquer dans la doc 👀 ) . C’est ce plugin qui va générer le fameux fichier et le poser à un endroit défini par Gradle.

Ensuite on va ajouter le test suivant dans notre classe

Ici nous avons donc modifié le contenu du buildFile pour invoquer notre plugin, ainsi que l’appel de GradleRunner avec withPluginClassPath() et withDebug(true). La première méthode permet de dire à GradleRunner de charger le fichier généré par le plugin java-gradle-plugin et la deuxième permet d’activer le débogueur dans le plugin testé. Vous pouvez maintenant lancer le test depuis votre IDE, tout devrait fonctionner correctement maintenant.

Si vous utilisez IntelliJ pour lancer directement vos tests, vous allez encore avoir le même problème. C’est un bug d’IntelliJ mais il existe un workaround qui conciste à changer le runner de test lancé par IntelliJ. Ça se passe dans Preferences > Build, Execution, Deployement > Build Tools > Gradle > Runner. Dans Run tests using : Il faut mettre Gradle Test Runner (cf : Stackoverflow)

Voilà vous êtes paré pour tester votre plugin, fini le println 😃

Les bonnes pratiques

Dans notre plugin, nous avons mis le code de notre tâche directement dans la classe MyFirstPlugin, de ce fait elle n’est pas réutilisable en l’état. Si dans un projet nous voulions réutiliser cette logique pour générer par exemple plusieurs fichiers ou autres, nous ne pourrions pas. Une bonne pratique est donc d’isoler le code dans une classe à part. Voyons voir ça.

Avec Gradle, une classe de tâche personnalisée doit hériter de DefaultTask et définir une méthode avec l’annotation @TaskAction

Ici nous avons simplement déplacé le code de la tâche dans notre méthode generateFile. A noter que nous n’utilisons pas l’extension pour récupérer le logSize et le filename mais nous passons par des propriétés de la classe. De cette façon cette tâche est réutilisable en dehors du contexte du plugin comme ceci

Concernant notre plugin, nous devons le modifier pour utiliser notre nouvelle tâche. Attention, ça va commencer à devenir intéressant !

Si nous le faisons de manière naïve comme ceci :

Et bien ça ne va pas fonctionner comme prévu. Quand nous allons exécuter la tâche info, les données que l’on aura configurées via le DSL ne seront pas prises en compte et les valeurs par défaut seront utilisées à la place. En effet Gradle définit plusieurs phases de build, dont Configuration et Exécution. Avant de déplacer notre code dans une classe à part, la déclaration de notre tâche ressemblait à ça :

L’extension était créée dans la phase de configuration mais était utilisée dans la phase d’exécution (doLast { …}), les valeurs étaient donc correctement évaluées. Avec la version naïve, tout est fait dans la phase de configuration et les valeurs ne sont donc pas correctement évaluées.

Pour résoudre ce problème, avant nous pouvions passer par la closure project.afterEvaluate { … } et appeler une méthode non documentée project.evaluate()dans nos tests pour forcer l’évaluation du plugin. Ce n’était pas terrible mais ça fonctionnait. Depuis Gradle 4.3, il y a maintenant une réponse officielle à ce problème. Il faut utiliser des Property<T>. Ce sont des propriétés lazy qui vont être déclarées dans la phase de configuration et évaluées dans la phase d’exécution. Voilà à quoi ça ressemble.

Ici nous avons modifié notre extension pour utiliser les fameux objets de type Property<T>. Les champs ne sont donc plus de simples types de base mais des sortes de “wrapper” construits par l’objet projet, lui-même injecté dans le constructeur.

Dans la classe MyFirstPlugin, pas beaucoup de changement par rapport à l’approche naïve si ce n’est qu’il faut maintenant passer l’objet project lors de la construction de l’extension.

Enfin dans la classe InfoTask, nous utilisons maintenant des objets Property qui vont être écrasés par ceux de l’extension. On ajoute en plus des “Setters” pour pouvoir continuer d’utiliser la tâche facilement en dehors du contexte du plugin comme vu précédemment. Et pour finir, dans la méthode generateFile nous avons changé les accès directs par des appels à la méthode get() pour récupérer la valeur de la propriété.

Et voilà, maintenant nous pouvons ajouter des tests unitaires pour valider le bon fonctionnement du passage des paramètres

Modularisation

Avec le temps notre plugin va grossir et on pourrait avoir envie de modulariser tout ça. Pour cela une autre bonne pratique consiste à découpler la logique métier de Gradle. Prenons par exemple le plugin Grgit. Avoir une lib en Groovy permettant d’interagir facilement avec des dépôts Git a du sens. Imaginons que je souhaite maintenant écrire une application en Groovy qui permette justement cela, ne serait-ce pas étrange de devoir dépendre de Gradle ? Et bien pour notre plugin c’est peut-être la même chose. Il faut voir si on peut isoler le code métier dans une bibliothèque à part et l’utiliser dans nos tâches Gradle. Sur le dépôt GitHub de l’article, la branche part2-modules représente une telle structure de projet. Il faudra cependant déployer la partie lib sur les dépôts jcenter en se créant un compte sur bintray si on veut continuer à utiliser le plugin avec la nouvelle notation.

Conclusion

La documentation de Gradle est très complète et il est facile de passer à côté d’informations essentielles. J’espère que ces 2 articles vous auront permis de rentrer plus facilement dans le développement de plugin Gradle. Il reste encore des choses à voir pour aller encore plus loin dans la définition du DSL avec les NamedDomainObjectContainer, peut-être dans un prochain article qui sait 😜

--

--