12 July 2025
ExtraPropertiesExtension
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
.
Le diagramme suivant illustre la déconnexion entre l’environnement de test et le système de fichiers :
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.
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).
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.
Commençons par créer la structure de base de notre test unitaire :
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...
}
}
Voici le processus détaillé pour injecter la propriété dans le projet de test :
@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"
)
}
Pour tester différents scénarios, créons plusieurs tests avec des configurations variées :
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) }
}
}
}
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.
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
}
}
}
@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
}
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.