Il problema: Isolare i test unitari di un plugin Gradle

Durante la scrittura di test unitari per un plugin Gradle, una sfida comune è gestire le dipendenze dalla configurazione del progetto, come le proprietà definite nel file`gradle.properties`. Nel nostro caso, il plugin`jbake.ghpages`devait leggere una proprietà`site_config_path`per funzionare correttamente. Il test unitario per il compito`initialize`doveva verificare il comportamento del plugin in presenza di questa proprietà.

Il problema fondamentale è che i test unitari, per progettazione, devono essere isolati. L’utilizzo di`ProjectBuilder`di Gradle crea un’istanza di`Project`in memoria, completamente scollegata da un vero progetto sul sistema di file. Di conseguenza, questa istanza di test non legge automaticamente il file`gradle.properties`e quindi non ha conoscenza della proprietà`site_config_path`.

Architettura del Problema

Il diagramma seguente illustra la disconnessione tra l’ambiente di test e il sistema di file:

test isolation problem

La tentazione di una falsa buona idea

Un primo approccio potrebbe essere di creare un file`gradle.properties`fittizio nelle risorse del test.

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

Tuttavia, questo metodo è destinato al fallimento perché`ProjectBuilder`non è progettato per eseguire la scansione del file system alla ricerca di file di configurazione. Il test rimarrebbe isolato e ignorerebbe questo file.

La Soluzione : Simulare la Proprietà con `ExtraPropertiesExtension

La soluzione elegante a questo problema non è leggere il file, ma simulare la presenza della proprietà direttamente nell’oggetto`Project`di test. Gradle fornisce un meccanismo potente per questo : le proprietà aggiuntive (Extra Properties).

Passo 1: Comprendere ExtraPropertiesExtension

`ExtraPropertiesExtension`è un contenitore chiave-valore collegato a ogni oggetto del modello Gradle. Consente di aggiungere proprietà dinamicamente a un progetto, una task o qualsiasi altra estensione Gradle.

extra properties architecture

Passo 2: Impostazione del Test - Preparazione

Cominciamo creando la struttura di base del nostro test unitario :

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

Passo 3 : Iniezione della Proprietà

Ecco il processo dettagliato per iniettare la proprietà nel progetto di 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"
    )
}

Passo 4 : Diagramma di Flusso della Soluzione

solution flow

Passo 5 : Confronto Prima/Dopo

before after comparison

Passo 6 : Gestione dei casi di test multipli

Per testare diversi scenari, creiamo più test con configurazioni varie:

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

Architettura finale della soluzione

final architecture

I vantaggi di questo approccio

  • Isolamento completoIl test non dipende da alcun file esterno. È autonomo e può essere eseguito in modo affidabile in qualsiasi ambiente (locale, CI/CD, ecc.).

  • Chiarezza e Intenzione: Il test dichiara esplicitamente le condizioni preliminari per la sua esecuzione. Chiunque legge il test vede immediatamente che il plugin richiede la proprietà`site_config_path`per funzionare.

  • Manutenibilità: Se il nome della proprietà cambia, basta aggiornarlo in un unico punto nel test, senza dover manipolare i file di configurazione di test.

  • Flessibilità: Diventa banale testare diversi scenari semplicemente cambiando il valore della proprietà iniettata in ogni test (valore valido, non valido, assente, ecc.).

  • Performance: Nessuna lettura di file, tutto avviene in memoria, il che rende i test più veloci.

  • Riproducibilità: I test sono deterministici perché non dipendono dallo stato del sistema di file.

Buone pratiche e consigli

Creare un metodo utilitario

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

Validazione delle Proprietà

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
}

Conclusione

Invece di lottare per far leggere i file di configurazione a un ambiente di test unitario, la pratica migliore consiste nel simulare lo stato richiesto. L’uso di`ExtraPropertiesExtension`Definire a livello di programmazione le proprietà Gradle è il metodo più pulito e robusto per realizzare test unitari di plugin efficaci e affidabili.

Questa tecnica trasforma un problema di dipendenza esterna in una semplice iniezione di dipendenza, al cuore stesso della filosofia del Test-Driven Development (TDD). Offre un controllo totale sull’ambiente di test mantenendo l’isolamento necessario per test unitari di qualità.

I diagrammi e gli esempi presentati in questo articolo mostrano come questo approccio possa essere implementato in modo progressivo e metodico, consentendo di creare una suite di test robusta e mantenibile per i tuoi plugin Gradle.

Articoli correlati