Het probleem: Het isoleren van eenheidstesten van een Gradle-plugin

Bij het schrijven van unit-tests voor een Gradle-plugin is een veelvoorkomende uitdaging om de afhankelijkheden van de projectconfiguratie te beheren, zoals eigenschappen gedefinieerd in het bestand`gradle.properties`. In ons geval, de plugin`jbake.ghpages`moest een eigenschap lezen`site_config_path`om correct te functioneren. De eenheidstest voor de taak`initialize`moest het gedrag van de plugin in aanwezigheid van deze eigenschap controleren.

Het fundamentele probleem is dat eenheidstests, per ontwerp, moeten zijn geïsoleerd. Het gebruik van`ProjectBuilder`Gradle maakt een instantie van`Project`in het geheugen, volledig losgekoppeld van een echt project op het bestandssysteem. Daarom leest deze testinstantie het bestand niet automatisch`gradle.properties`en heeft dus geen kennis van het eigendom`site_config_path`.

Architectuur van het Probleem

Het volgende diagram illustreert de ontkoppeling tussen de testomgeving en het bestandssysteem :

@startuml
!define RECTANGLE class

package "Productieomgeving" {
  RECTANGLE ProjectReal {
    + build.gradle.kts
    + gradle.properties
    + src/
  }

  RECTANGLE GradleProperties {
    site_config_path=src/jbake/settings/site.yml
  }

  RECTANGLE JbakePlugin {
    - readProperty(name: String)
    + initialize()
  }

  ProjectReal --> GradleProperties : Leest bij het opstarten
  JbakePlugin --> GradleProperties : toegang via project.properties
}

package "Testomgeving" {
  RECTANGLE ProjectTest {
    + Créé par ProjectBuilder
    + En mémoire uniquement
    + Pas de gradle.properties
  }

  RECTANGLE JbakePluginTest {
    + testInitialize()
  }

  ProjectTest -[#red]x GradleProperties : ❌ Pas d'accès
  JbakePluginTest --> ProjectTest : gebruikt
}

note right of ProjectTest : Geen toegang\ntot het bestandssysteem
@enduml

De Verleiding van een Foute Goede Idee

Een eerste benadering zou kunnen zijn om een bestand aan te maken.`gradle.properties`fictief in de bronnen van de test.

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

Echter, deze methode is gedoemd om te falen omdat`ProjectBuilder`is niet ontworpen om het bestandssysteem te scannen op zoek naar configuratiebestanden. De test zou geïsoleerd blijven en dit bestand negeren.

De oplossing: Eigendom simuleren met `ExtraPropertiesExtension

De elegante oplossing voor dit probleem is niet om lezen het bestand, maar om simuleren de aanwezigheid van de eigenschap direct in het object`Project`van test. Gradle biedt een krachtig mechanisme voor dit: propriétés supplémentaires (Extra Properties).

Stap 1 : Begrijpen ExtraPropertiesExtension

`ExtraPropertiesExtension`is een key-value-container dat is gekoppeld aan elk object in het Gradle-model. Het stelt in staat om dynamisch eigenschappen toe te voegen aan een project, een taak, of elke andere Gradle-extensie.

@startuml
!define RECTANGLE class

RECTANGLE Project {
  + name: String
  + version: String
  + extensions: ExtensionContainer
  + properties: Map<String, ?>
}

RECTANGLE ExtensionContainer {
  + getByType(Class<T>): T
  + add(String, Object): void
}

RECTANGLE ExtraPropertiesExtension {
  + set(String, Object): void
  + get(String): Object
  + has(String): boolean
  + getProperties(): Map<String, Object>
}

Project --> ExtensionContainer : bevat
ExtensionContainer --> ExtraPropertiesExtension : beheert
ExtraPropertiesExtension --> Project : synchroniseer met\nproject.properties

note right of ExtraPropertiesExtension : Ingangspunt voor\ninjecteren de eigenschappen
@enduml

Stap 2 : Testopzet - Voorbereiding

Laten we beginnen met het maken van de basisstructuur van onze eenheidstest :

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...
    }
}

Stap 3 : Injectie van de eigenschap

Dit is het gedetailleerde proces om de eigenschap in het testproject te injecteren:

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"
    )
}

Stap 4 : Stroomdiagram van de Oplossing

@startuml
start

:Maken ProjectBuilder;
:Verkrijgen ExtraPropertiesExtension;
:Injecteer de eigenschap\nvia extra.set();

partition "Verificaties" {
  :Controleren project.hasProperty();
  :Toepassen van de plug‑in;
  :De taak ophalen;
}

partition "Uitvoering" {
  :Voer de acties\nvan de taak uit;
  :De plugin leest\nproject.properties;
}

partition "Asserties" {
  :Controleer de eigenschap\nis toegankelijk;
  :Valideer het gedrag\nverwachte;
}

stop
@enduml

Stap 5 : vergelijking voor/na

@startuml
!define RECTANGLE class

package "AVANT - Probleem" {
  RECTANGLE TestProject1 {
    + ProjectBuilder.build()
    + Pas de propriétés
  }

  RECTANGLE Plugin1 {
    + initialize()
    + ❌ project.properties["site_config_path"] = null
  }

  TestProject1 --> Plugin1 : faal

  note right of Plugin1 : Test faalt omdat\nde eigenschap niet bestaat
}

package "APRÈS - Oplossing" {
  RECTANGLE TestProject2 {
    + ProjectBuilder.build()
    + ExtraPropertiesExtension
  }

  RECTANGLE ExtraProps {
    + set("site_config_path", "...")
  }

  RECTANGLE Plugin2 {
    + initialize()
    + ✅ project.properties["site_config_path"] = "src/jbake/settings/site.yml"
  }

  TestProject2 --> ExtraProps : configureren
  ExtraProps --> Plugin2 : beschikbaar eigendom

  note right of Plugin2 : Test slaagt omdat\nde eigenschap gesimuleerd wordt
}
@enduml

Stap 6: Beheer van meerdere testcases

Voor het testen van verschillende scenario’s laten we meerdere tests maken met verschillende configuraties :

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) }
        }
    }
}

Finale architectuur van de oplossing

@startuml
!define RECTANGLE class

package "Testomgeving" {
  RECTANGLE TestClass {
    + createProjectWithProperty()
    + testValidProperty()
    + testMissingProperty()
    + testInvalidProperty()
  }

  RECTANGLE ProjectBuilder {
    + builder()
    + withProjectDir()
    + build()
  }

  RECTANGLE TestProject {
    + extensions
    + properties
    + plugins
    + tasks
  }

  RECTANGLE ExtraPropertiesExtension {
    + set(key, value)
    + get(key)
    + has(key)
  }

  RECTANGLE JbakePlugin {
    + apply(project)
    + createInitializeTask()
    + readSiteConfigPath()
  }
}

TestClass --> ProjectBuilder : gebruikt
ProjectBuilder --> TestProject : gemaakt
TestProject --> ExtraPropertiesExtension : bevat
TestClass --> ExtraPropertiesExtension : configureren
ExtraPropertiesExtension --> TestProject : synchroniseer eigenschappen
TestProject --> JbakePlugin : toepast
JbakePlugin --> TestProject : lit eigenschappen

note right of ExtraPropertiesExtension : Controlepunt\nvoor de injectie
note bottom of TestProject : Omgeving\ngecontroleerd en geïsoleerd
@enduml

De voordelen van deze aanpak

  • Volledige isolatie: De test is niet afhankelijk van enige externe bestanden. Het is zelfstandig en kan betrouwbaar worden uitgevoerd in elke omgeving (lokaal, CI/CD, enz.).

  • Duidelijkheid en intentieDe test verklaart expliciet de voorwaarden voor zijn uitvoering. Iedereen die het test leest, ziet onmiddellijk dat de plugin de eigenschap vereist`site_config_path`om te functioneren.

  • OnderhoudbaarheidAls de naam van de eigenschap verandert, hoeft u het alleen op één plek in de test bij te werken, zonder testconfiguratiebestanden te hoeven manipuleren.

  • Flexibiliteit: Het wordt triviaal om verschillende scenario’s te testen door simpelweg de waarde van de ingeinjecteerde eigenschap in elke test te wijzigen (geldige waarde, ongeldige waarde, afwezig, enz.).

  • Prestatie: Geen lezen van bestanden, alles gebeurt in het geheugen, wat de tests sneller maakt.

  • reproduceerbaarheid: De tests zijn deterministisch omdat ze niet afhangen van de staat van het bestandssysteem.

Goede Praktijken en Tips

Maak een utiliteitsmethode aan

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
        }
    }
}

Validatie van de eigenschappen

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
}

Conclusie

In plaats van te worstelen om een eenheidstestomgeving configuratiebestanden te laten lezen, bestaat de beste praktijk erin de vereiste staat te simuleren. Het gebruik van`ExtraPropertiesExtension`Programmatisch de Gradle-eigenschappen definiëren is de zuiverste en meest robuuste manier om efficiënte en betrouwbare eenheidstests van plugins uit te voeren.

Deze techniek transformeert een probleem van externe afhankelijkheid in een eenvoudige afhankelijkheidsinjectie, midden in de filosofie van Test-Driven Development (TDD). Ze biedt volledige controle over de testomgeving, terwijl de nodige isolatie voor kwalitatieve unittests behouden blijft.

De diagrammen en voorbeelden die in dit artikel worden gepresenteerd laten zien hoe deze aanpak progressief en methodisch kan worden geïmplementeerd, waardoor een robuuste en onderhoudbare testsuite kan worden gemaakt voor uw Gradle‑plugins.

Gerelateerde artikelen