Masalah: Mengisolasi Uji Unit dari Plugin Gradle

Saat menulis tes unit untuk plugin Gradle, tantangan umum adalah mengelola dependensi terhadap konfigurasi proyek, seperti properti yang ditentukan dalam file`gradle.properties`. Dalam kasus kita, plugin`jbake.ghpages`harus membaca sebuah properti`site_config_path`untuk berfungsi dengan baik. Uji unit untuk tugas`initialize`harus memeriksa perilaku plugin saat properti ini ada.

Masalah fundamentalnya adalah bahwa tes unit, menurut desainnya, harus terisolasi. Penggunaan`ProjectBuilder`Gradle membuat sebuah instance dari`Project`dalam memori, sepenuhnya terputus dari proyek sebenarnya pada sistem file. Oleh karena itu, instance tes ini tidak membaca file secara otomatis.gradle.properties`dan karena itu tidak mengetahui properti`site_config_path.

Arsitektur Masalah

Diagram berikut menggambarkan pemisahan antara lingkungan uji dan sistem file:

@startuml
!define RECTANGLE class

package "Lingkungan Produksi" {
  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 : lit au démarrage
  JbakePlugin --> GradleProperties : accède via project.properties
}

package "Lingkungan Test" {
  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 : utilise
}

note right of ProjectTest : Isolation = Pas d'accès\nau système de fichiers
@enduml

Godaan dari Gagasan Baik yang Salah

Salah satu pendekatan pertama mungkin adalah membuat file`gradle.properties`palsu dalam sumber daya ujian.

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

Namun, metode ini ditakdirkan untuk gagal karena`ProjectBuilder`tidak dirancang untuk memindai sistem file dalam mencari file konfigurasi. Ujian akan tetap terisolasi dan mengabaikan file ini.

Solusi: Mensimulasikan properti dengan `ExtraPropertiesExtension

Solusi yang anggun untuk masalah ini bukanlah membaca file, tetapi mensimulasikan kehadiran properti langsung dalam objek`Project`uji. Gradle menyediakan mekanisme yang kuat untuk ini: properti tambahan (Extra Properties).

Langkah 1 : Memahami ExtraPropertiesExtension

`ExtraPropertiesExtension`adalah sebuah kontainer kunci-nilai yang terlampir pada setiap objek model Gradle. Ia memungkinkan penambahan properti secara dinamis ke sebuah proyek, sebuah tugas, atau ekstensi Gradle lain.

@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 : contient
ExtensionContainer --> ExtraPropertiesExtension : gère
ExtraPropertiesExtension --> Project : synchronise avec\nproject.properties

note right of ExtraPropertiesExtension : Point d'entrée pour\ninjecter des propriétés
@enduml

Langkah 2: Penyiapan Tes - Persiapan

Mari kita mulai dengan membuat struktur dasar dari tes unit kita :

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

Langkah 3: Injeksi Properti

Berikut proses detail untuk menyuntikkan properti ke dalam proyek uji :

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

Langkah 4 : Diagram Alur Solusi

@startuml
start

:Créer ProjectBuilder;
:Obtenir ExtraPropertiesExtension;
:Injecter la propriété\nvia extra.set();

partition "verifikasi" {
  :Vérifier project.hasProperty();
  :Appliquer le plugin;
  :Récupérer la tâche;
}

partition "pelaksanaan" {
  :Exécuter les actions\nde la tâche;
  :Le plugin lit\nproject.properties;
}

partition "Pernyataan" {
  :Vérifier la propriété\nest accessible;
  :Valider le comportement\nattendues;
}

stop
@enduml

Langkah 5 : Perbandingan Sebelum/Sesudah

@startuml
!define RECTANGLE class

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

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

  TestProject1 --> Plugin1 : échec

  note right of Plugin1 : Test échoue car\nla propriété n'existe pas
}

package "Setelah - Solusi" {
  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 : configure
  ExtraProps --> Plugin2 : propriété disponible

  note right of Plugin2 : Test réussit car\nla propriété est simulée
}
@enduml

Langkah 6 : Pengelolaan Kasus Pengujian Ganda

Untuk menguji berbagai skenario, mari buat beberapa tes dengan konfigurasi yang beragam :

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

Arsitektur Akhir Solusi

@startuml
!define RECTANGLE class

package "lingkungan uji" {
  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 : utilise
ProjectBuilder --> TestProject : crée
TestProject --> ExtraPropertiesExtension : contient
TestClass --> ExtraPropertiesExtension : configure
ExtraPropertiesExtension --> TestProject : synchronise properties
TestProject --> JbakePlugin : applique
JbakePlugin --> TestProject : lit properties

note right of ExtraPropertiesExtension : Point de contrôle\npour l'injection
note bottom of TestProject : Environnement\ncontrôlé et isolé
@enduml

Manfaat Pendekatan Ini

  • Isolasi LengkapUji tidak bergantung pada file eksternal apa pun. Ia mandiri dan dapat dieksekusi dengan andal di mana-mana lingkungan (lokal, CI/CD, dst.).

  • Kejelasan dan Niat: Test secara eksplisit menyatakan prasyarat untuk eksekusinya. Siapa pun yang membaca tes akan langsung melihat bahwa plugin membutuhkan properti`site_config_path`untuk berfungsi.

  • kemudahan pemeliharaan: Jika nama properti berubah, cukup memperbaruinya di satu tempat dalam tes, tanpa perlu memanipulasi file konfigurasi tes.

  • fleksibilitas: Menjadi trivial untuk menguji berbagai skenario hanya dengan mengubah nilai properti yang di-injeksi pada setiap test (nilai valid, tidak valid, tidak ada, dsb.).

  • KinerjaTidak ada pembacaan file, semuanya berlangsung di memori, yang membuat tes lebih cepat.

  • kecocokanPengujian bersifat deterministik karena tidak tergantung pada keadaan sistem berkas.

Praktik terbaik dan saran

Buat Metode Utilitas

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

Validasi properti

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
}

Kesimpulan

Alih-alih berjuang agar lingkungan pengujian unit membaca file konfigurasi, praktik terbaiknya adalah mensimulasikan kondisi yang diperlukan. Penggunaan`ExtraPropertiesExtension`Untuk menentukan secara program properti Gradle adalah metode yang paling bersih dan paling kuat untuk melakukan tes unit plugin yang efektif dan andal.

Teknik ini mengubah masalah dependensi eksternal menjadi injeksi dependensi sederhana, di tengah-tengah filosofi Test-Driven Development (TDD). Ia memberikan kontrol penuh atas lingkungan ujian sekaligus menjaga isolasi yang diperlukan untuk pengujian unit berkualitas.

Diagram dan contoh yang disajikan dalam artikel ini menunjukkan bagaimana pendekatan ini dapat diimplementasikan secara progresif dan metodis, memungkinkan pembuatan rangkaian uji yang kuat dan dapat dipelihara untuk plugin Gradle Anda.

Artikel terkait