Article 2 : Développer un Plugin Gradle avec une Approche TDD
Publié le 24 September 2025
Dans cet article, nous allons explorer comment mettre en place une base de développement solide pour un plugin Gradle en utilisant une approche de Développement Guidé par les Tests (TDD - Test-Driven Development). Cette méthode nous assure que notre code est robuste, maintenable et répond précisément aux exigences dès le départ.
1. 1. L’Initialisation du Projet
Gradle facilite grandement la création d’un nouveau plugin grâce à la commande gradle init. En choisissant de créer un "Gradle Plugin" avec Kotlin, nous obtenons une structure de projet prête à l’emploi, incluant deux types de tests cruciaux :
- 
Tests Unitaires : Situés dans
src/test, ils permettent de valider des composants isolés de notre plugin, comme la logique interne d’une tâche ou la configuration d’une extension. Ils sont rapides et n’ont pas besoin d’une exécution complète de Gradle. - 
Tests Fonctionnels : Situés dans
src/functionalTest, ils utilisentGradleRunnerpour exécuter une build Gradle complète dans un projet de test temporaire. Cela nous permet de vérifier le comportement réel du plugin dans un environnement contrôlé. 
2. 2. Notre Premier Cycle TDD : Enregistrer une Tâche
Notre première exigence est simple : le plugin doit enregistrer une tâche nommée printSiteConfig.
2.1. 2.1. Le Test d’Abord (Le Test qui Échoue)
Conformément au TDD, nous écrivons d’abord un test qui vérifie l’existence de cette tâche. Dans SiteBakerPluginTest.kt (notre fichier de test unitaire), nous ajoutons :
@Test
fun `plugin registers task`() {
    // Créer un projet de test en mémoire
    val project = ProjectBuilder.builder().build()
    project.plugins.apply("com.cheroliv.site-baker")
    // Vérifier que la tâche a bien été enregistrée
    assertNotNull(project.tasks.findByName("printSiteConfig"))
}
Ce test échoue, car nous n’avons encore écrit aucun code dans notre plugin.
2.2. 2.2. Le Code Ensuite (Faire Passer le Test)
Maintenant, nous écrivons le minimum de code nécessaire dans SiteBakerPlugin.kt pour que le test passe :
class SiteBakerPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        // Enregistrer une tâche simple
        project.tasks.register("printSiteConfig") { task ->
            // ... la logique de la tâche viendra plus tard
        }
    }
}
Nous relançons les tests, et ils passent. Notre première fonctionnalité est validée.
3. 3. Deuxième Cycle TDD : Ajouter une Extension DSL
L’exigence suivante est de permettre aux utilisateurs de configurer notre plugin via un bloc DSL dans leur build.gradle.kts. Nous voulons un bloc site { … } où l’on peut spécifier un chemin de configuration.
3.1. 3.1. Le Test d’Abord
Nous ajoutons un test pour vérifier que l’extension site est bien enregistrée et qu’on peut y affecter une valeur.
@Test
fun `plugin registers extension`() {
    val project = ProjectBuilder.builder().build()
    project.plugins.apply("com.cheroliv.site-baker")
    // Récupérer l'extension et lui affecter une valeur
    project.extensions
        .findByType(SiteExtension::class.java)!!
        .configPath
        .set("config.yml")
    // Vérifier que la valeur a bien été prise en compte
    assertEquals(
        "config.yml",
        project.extensions.findByType(SiteExtension::class.java)?.configPath?.get()
    )
}
Ce test échoue car ni la classe SiteExtension ni l’enregistrement de l’extension n’existent.
3.2. 3.2. Le Code Ensuite
Nous créons la classe SiteBakerExtension.kt et mettons à jour SiteBakerPlugin.kt :
// SiteBakerExtension.kt
open class SiteExtension @Inject constructor(objects: ObjectFactory) {
    val configPath: Property<String> = objects.property(String::class.java)
}
// SiteBakerPlugin.kt
class SiteBakerPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        // Enregistrer l'extension
        val extension = project.extensions.create("site", SiteExtension::class.java)
        project.tasks.register("printSiteConfig") { task ->
            task.doLast {
                // On utilisera l'extension plus tard
            }
        }
    }
}
Les tests passent à nouveau.
4. 4. Le Test Fonctionnel : Valider l’Intégration
Maintenant que les unités sont testées, nous devons nous assurer que tout fonctionne ensemble dans une vraie build. C’est le rôle du test fonctionnel dans SiteBakerPluginFunctionalTest.kt.
4.1. 4.1. Le Test d’Intégration : Créer un Environnement Contrôlé
Ce test va simuler un vrai projet utilisant notre plugin. Pour qu’il soit fiable, il doit être hermétique, c’est-à-dire qu’il ne doit pas dépendre de fichiers existants sur le système. Il doit créer lui-même toutes les conditions nécessaires à son exécution.
Le test va donc :
1.  Créer un build.gradle.kts de test qui utilise notre plugin et son DSL.
2.  Créer le fichier de configuration (managed-jbake-context.yml) que le plugin est censé lire. C’est une étape cruciale pour la robustesse du test.
3.  Exécuter la tâche printSiteConfig via GradleRunner.
4.  Vérifier que la sortie de la build est correcte.
En copiant ou créant ce fichier de configuration à chaque exécution, nous nous assurons que le test est reproductible et ne dépend pas d’un état externe. Cela sécurise nos tests contre les régressions qui pourraient être liées à la lecture du fichier.
@Test fun `can run task with DSL`() {
    // 1. Créer un build.gradle.kts de test
    buildFile.writeText("""
        plugins { id("com.cheroliv.site-baker") }
        site { configPath = "managed-jbake-context.yml" }
    """.trimIndent())
    // 2. Créer le fichier de configuration pour un test contrôlé
    val configFile = File(projectDir, "managed-jbake-context.yml")
    configFile.writeText("site: { title: 'Mon Site de Test' }") // Contenu YAML simple
    // 3. Exécuter la build
    val runner = GradleRunner.create()
    runner.withPluginClasspath()
    runner.withArguments("printSiteConfig")
    runner.withProjectDir(projectDir) // Spécifier le répertoire du projet de test
    val result = runner.build()
    // 4. Vérifier la sortie
    assertTrue(result.output.contains("Site config path: managed-jbake-context.yml"))
}
Ce test échouera tant que la tâche printSiteConfig n’utilisera pas réellement la valeur de l’extension.
4.2. 4.2. Finaliser la Logique
Nous mettons à jour la tâche dans SiteBakerPlugin.kt pour qu’elle affiche la valeur configurée :
project.tasks.register("printSiteConfig") { task ->
    task.doLast {
        println("Site config path: ${extension.configPath.get()}")
    }
}
Tous les tests, unitaires et fonctionnels, passent désormais.
5. Conclusion
En suivant une approche TDD, nous avons construit un plugin de manière incrémentale et sécurisée. Chaque petite fonctionnalité est immédiatement validée par un test, des tests unitaires rapides aux tests fonctionnels qui valident l’intégration complète.
En prenant soin de rendre nos tests fonctionnels hermétiques — notamment en créant programmatiquement les fichiers de configuration nécessaires — nous bâtissons un filet de sécurité extrêmement robuste. Cette rigueur nous protège efficacement contre les régressions et nous donne une grande confiance pour ajouter des fonctionnalités plus complexes par la suite, comme le parsing du fichier de configuration YAML.
Cette base de tests solides est l’atout le plus précieux pour la maintenance et l’évolution future du plugin.