Résoudre le Défi des Tests Unitaires Gradle avec `gradle.properties`

12 July 2025

Sommaire

1. Le Problème : Isoler les Tests Unitaires d’un Plugin Gradle

Lors de l’écriture de tests unitaires pour un plugin Gradle, un défi courant est de gérer les dépendances à la configuration du projet, comme les propriétés définies dans le fichier gradle.properties. Dans notre cas, le plugin jbake.ghpages devait lire une propriété site_config_path pour fonctionner correctement. Le test unitaire pour la tâche initialize devait vérifier le comportement du plugin en présence de cette propriété.

Le problème fondamental est que les tests unitaires, par conception, doivent être isolés. L’utilisation de ProjectBuilder de Gradle crée une instance de Project en mémoire, complètement déconnectée d’un véritable projet sur le système de fichiers. Par conséquent, cette instance de test ne lit pas automatiquement le fichier gradle.properties et n’a donc pas connaissance de la propriété site_config_path.

1.1. Architecture du Problème

Le diagramme suivant illustre la déconnexion entre l’environnement de test et le système de fichiers :

test isolation problem

1.2. La Tentation d’une Fausse Bonne Idée

Une première approche pourrait être de créer un fichier gradle.properties factice dans les ressources du test.

// src/test/resources/gradle.properties
site_config_path=src/jbake/settings/site.yml

Cependant, cette méthode est vouée à l’échec car ProjectBuilder n’est pas conçu pour scanner le système de fichiers à la recherche de fichiers de configuration. Le test resterait isolé et ignorerait ce fichier.

2. La Solution : Simuler la Propriété avec ExtraPropertiesExtension

La solution élégante à ce problème n’est pas de lire le fichier, mais de simuler la présence de la propriété directement dans l’objet Project de test. Gradle fournit un mécanisme puissant pour cela : les propriétés supplémentaires (Extra Properties).

2.1. Étape 1 : Comprendre ExtraPropertiesExtension

ExtraPropertiesExtension est un conteneur clé-valeur attaché à chaque objet du modèle Gradle. Il permet d’ajouter des propriétés dynamiquement à un projet, une tâche, ou toute autre extension Gradle.

extra properties architecture

2.2. Étape 2 : Mise en Place du Test - Préparation

Commençons par créer la structure de base de notre test unitaire :

JbakeGhPagesPluginTest.kt - Structure de base
class JbakeGhPagesPluginTest {

    @TempDir
    lateinit var testProjectDir: File

    @Test
    fun `check initialize and config yaml file if not existing`() {
        // Étape 1 : Créer un projet de test isolé
        val project = ProjectBuilder.builder()
            .withProjectDir(testProjectDir)
            .build()

        // Suite des étapes...
    }
}

2.3. Étape 3 : Injection de la Propriété

Voici le processus détaillé pour injecter la propriété dans le projet de test :

JbakeGhPagesPluginTest.kt - Injection complète
@Test
fun `check initialize and config yaml file if not existing`() {
    // Étape 1 : Créer un projet de test isolé en mémoire
    val project = ProjectBuilder.builder()
        .withProjectDir(testProjectDir)
        .build()

    // Étape 2 : Récupérer le gestionnaire de propriétés supplémentaires
    val extra = project.extensions.getByType(ExtraPropertiesExtension::class.java)

    // Étape 3 : Définir la propriété requise pour ce test
    extra.set("site_config_path", "src/jbake/settings/site.yml")

    // Étape 4 : Vérifier que la propriété est bien injectée
    assertTrue(project.hasProperty("site_config_path"))

    // Étape 5 : Appliquer le plugin qui pourra maintenant accéder à la propriété
    project.plugins.apply("jbake.ghpages")

    // Étape 6 : Exécuter la tâche et vérifier son comportement
    val task: Task = project.tasks.findByName("initialize")
        .apply(::assertNotNull)!!

    // Étape 7 : Exécuter les actions de la tâche
    task.actions.forEach { it.execute(task) }

    // Étape 8 : Assertions finales
    assertEquals(
        "src/jbake/settings/site.yml",
        project.properties["site_config_path"],
        "La propriété devrait être accessible dans le projet"
    )
}

2.4. Étape 4 : Diagramme de Flux de la Solution

solution flow

2.5. Étape 5 : Comparaison Avant/Après

before after comparison

2.6. Étape 6 : Gestion des Cas de Test Multiples

Pour tester différents scénarios, créons plusieurs tests avec des configurations variées :

Tests multiples avec configurations différentes
class JbakeGhPagesPluginTest {

    @TempDir
    lateinit var testProjectDir: File

    private fun createProjectWithProperty(propertyValue: String?): Project {
        val project = ProjectBuilder.builder()
            .withProjectDir(testProjectDir)
            .build()

        // Injection conditionnelle de la propriété
        propertyValue?.let { value ->
            val extra = project.extensions.getByType(ExtraPropertiesExtension::class.java)
            extra.set("site_config_path", value)
        }

        return project
    }

    @Test
    fun `should work with valid property`() {
        val project = createProjectWithProperty("src/jbake/settings/site.yml")
        project.plugins.apply("jbake.ghpages")

        val task = project.tasks.findByName("initialize")!!
        task.actions.forEach { it.execute(task) }

        // Assertions pour le cas normal
        assertEquals("src/jbake/settings/site.yml", project.properties["site_config_path"])
    }

    @Test
    fun `should handle missing property gracefully`() {
        val project = createProjectWithProperty(null) // Pas de propriété
        project.plugins.apply("jbake.ghpages")

        val task = project.tasks.findByName("initialize")!!

        // Le plugin devrait gérer l'absence de propriété
        assertDoesNotThrow {
            task.actions.forEach { it.execute(task) }
        }
    }

    @Test
    fun `should handle invalid property path`() {
        val project = createProjectWithProperty("invalid/path/to/config.yml")
        project.plugins.apply("jbake.ghpages")

        val task = project.tasks.findByName("initialize")!!

        // Test du comportement avec un chemin invalide
        assertThrows<FileNotFoundException> {
            task.actions.forEach { it.execute(task) }
        }
    }
}

2.7. Architecture Finale de la Solution

final architecture

3. Les Avantages de cette Approche

  • Isolation Complète : Le test ne dépend d’aucun fichier externe. Il est autonome et peut s’exécuter de manière fiable dans n’importe quel environnement (local, CI/CD, etc.).

  • Clarté et Intention : Le test déclare explicitement les conditions préalables à son exécution. Quiconque lit le test voit immédiatement que le plugin nécessite la propriété site_config_path pour fonctionner.

  • Maintenabilité : Si le nom de la propriété change, il suffit de le mettre à jour à un seul endroit dans le test, sans avoir à manipuler des fichiers de configuration de test.

  • Flexibilité : Il devient trivial de tester différents scénarios en changeant simplement la valeur de la propriété injectée dans chaque test (valeur valide, invalide, absente, etc.).

  • Performance : Pas de lecture de fichiers, tout se passe en mémoire, ce qui rend les tests plus rapides.

  • Reproductibilité : Les tests sont déterministes car ils ne dépendent pas de l’état du système de fichiers.

4. Bonnes Pratiques et Conseils

4.1. Créer une Méthode Utilitaire

Méthode utilitaire pour la réutilisation
class GradleTestUtils {
    companion object {
        fun createProjectWithProperties(
            projectDir: File,
            properties: Map<String, String>
        ): Project {
            val project = ProjectBuilder.builder()
                .withProjectDir(projectDir)
                .build()

            val extra = project.extensions.getByType(ExtraPropertiesExtension::class.java)
            properties.forEach { (key, value) ->
                extra.set(key, value)
            }

            return project
        }
    }
}

4.2. Validation des Propriétés

Validation robuste
@Test
fun `should validate property injection`() {
    val project = createProjectWithProperty("test-value")

    // Vérifications multiples
    assertTrue(project.hasProperty("site_config_path"))
    assertEquals("test-value", project.property("site_config_path"))
    assertNotNull(project.properties["site_config_path"])

    // Vérification que la propriété est accessible par le plugin
    project.plugins.apply("jbake.ghpages")
    // ... reste du test
}

5. Conclusion

Plutôt que de lutter pour faire lire des fichiers de configuration à un environnement de test unitaire, la meilleure pratique consiste à simuler l’état requis. L’utilisation de ExtraPropertiesExtension pour définir par programme les propriétés Gradle est la méthode la plus propre et la plus robuste pour réaliser des tests unitaires de plugins efficaces et fiables.

Cette technique transforme un problème de dépendance externe en une simple injection de dépendance, au cœur même de la philosophie du Test-Driven Development (TDD). Elle offre un contrôle total sur l’environnement de test tout en maintenant l’isolation nécessaire à des tests unitaires de qualité.

Les diagrammes et exemples présentés dans cet article montrent comment cette approche peut être mise en œuvre de manière progressive et méthodique, permettant de créer une suite de tests robuste et maintenable pour vos plugins Gradle.