Sorun : Gradle Eklentisinin Birim Testlerini İzole Etme

Gradle eklentisi için birim testleri yazılırken, bir yaygın zorluk, proje yapılandırmasına bağlı bağımlılıkları yönetmektir, dosyada tanımlanmış özellikler gibi.gradle.properties. Bizim durumumuzda, eklenti`jbake.ghpages`Bir özellik okumalıydı`site_config_path`duzgunca calismak icin. Birim testi görevi icin`initialize`bu özelliğin varlığında eklentinin davranışını kontrol etmesi gerekiyordu.

Temel sorun, birim testlerinin tasarım gereği izole olmaları gerektiğidir. Kullanım`ProjectBuilder`Gradle’ün bir örneği oluşturur`Project`Bellekte, dosya sistemindeki gerçek bir projedan tamamen ayrılmış. Bu nedenle, bu test örneği dosyayı otomatik olarak okumuyor.gradle.properties`ve dolayısıyla mülk hakkında bilgi sahibi değildir`site_config_path.

Problemin Mimarisi

Aşağıdaki diyagram, test ortamı ile dosya sistemi arasındaki bağlantı kopamasını gösterir:

@startuml
!define RECTANGLE class

package "Environnement de Production" {
  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 "Environnement de 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

Yanlış İyi Bir Fikir Çekinceliği

Bir ilk yaklaşım bir dosya oluşturmak olabilirdi.`gradle.properties`test kaynaklarında sahte.

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

Ancak, bu yöntem başarısız olmaya mahkûm olduğu için`ProjectBuilder`Yapılandırma dosyalarını aramak için dosya sistemini taramak üzere tasarlanmamıştır. Test izole kalırdı ve bu dosyayı görmezden gelirdi.

Çözüm : Mülkiyet’i simüle etmek ExtraPropertiesExtension ile

Bu problemi çözmenin şık yolu, dosyayı okumak değil, özelliğin nesneye doğrudan simüle edilmesidir.`Project`test için. Gradle bunun için güçlü bir mekanizma sağlar: ek özellikler (Extra Properties).

Adım 1: ExtraPropertiesExtension’i Anlamak

`ExtraPropertiesExtension`Gradle modelindeki her nesneye eklenen bir anahtar-değer konteynerdir. Projeye, göreve veya başka bir Gradle uzantısına özellik dinamik olarak eklemek için kullanılır.

@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

Adım 2: Test Kurulumu - Hazırlık

Birim testimizin temel yapısını oluşturarak başlayalım:

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

Adım 3: Özellik Enjeksiyonu

İşte test projesine özellik enjekte etme sürecinin ayrıntılı adımları :

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

Adım 4: Çözüm Akış Diyagramı

@startuml
start

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

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

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

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

stop
@enduml

Adım 5 : Önce/Sonra Karşılaştırma

@startuml
!define RECTANGLE class

package "AVANT - Problématique" {
  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 "APRÈS - Solution" {
  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

6. Adım: Çoklu Test Durumlarının Yönetimi

Farklı senaryoları test etmek için çeşitli yapılandırmalar içeren birden fazla test oluşturalım :

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

Çözümün Nihai Mimarisi

@startuml
!define RECTANGLE class

package "Test Environment" {
  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

Bu Yaklaşımın Avantajları

  • Tam İzolasyonTest, harici bir dosyaya bağımlı değildir. Bağımsız ve herhangi bir ortamda (yerel, CI/CD vb.) güvenilir bir şekilde çalıştırılabilir.

  • netlik ve niyet: Test, yürütülmesi için ön koşulları açıkça beyan eder. Testi okuyan herkes, hemen eklentinin özelliğe ihtiyaç duyduğunu görür`site_config_path`işlemek için.

  • bakım kolaylığıÖzelliğin adı değiştiğinde, test yapılandırma dosyalarıyla uğraşmak zorunda kalmadan sadece testin tek bir yerinde güncellemek yeterlidir.

  • EsneklikFarklı senaryoları test etmek sadece her testte enjekte edilen özelliğin değerini değiştirerek oldukça basit hale gelir (geçerli değer, geçersiz değer, eksik değer vb.).

  • PerformansDosya okuma yapılmaz, her şey bellekte gerçekleşir, bu da testleri daha hızlı kılar.

  • Tekrar üretilebilirlikTestler deterministiktir çünkü dosya sisteminin durumuna bağlı değiller.

İyi Uygulamalar ve Tavsiyeler

Bir yardımcı yöntem oluştur

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

Özelliklerin doğrulanması

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
}

Sonuç

Birim testi ortamının yapılandırma dosyalarını okumak için uğraşmak yerine, gerekli durumu simülemek en iyi uygulamadır. Kullanım`ExtraPropertiesExtension`Programatik olarak Gradle özelliklerini tanımlamak, etkili ve güvenilir eklenti birim testleri gerçekleştirmenin en temiz ve en sağlam yöntemidir.

Bu teknik, dış bağımlılık sorununu Test-Driven Development (TDD)哲学的核心中的简单依赖注入。Test ortamını tam olarak kontrol ederken, kaliteli birim testleri için gereken izolasyonu korur.

Bu makalede sunulan diyagramlar ve örnekler, bu yaklaşımın adım adım ve metodolojik olarak nasıl uygulanabileceğini gösterir, böylece Gradle eklentileriniz için sağlam ve sürdürülebilir bir test paketi oluşturmanızı sağlar.

İlgili Makaleler