مسئلہ : گریڈل پلاگِن کے یونٹ ٹیسٹس کو الگ کرنا

گریڈل پلاگین کے لئے یونٹ ٹیسٹس لکھتے وقت، ایک عام چیلنج پروجیکٹ کی تنظیمی ساتھ ڈپینڈنسیز کو منیج کرنا ہے، جیسے فائل میں تعریف کردہ خصوصیات`gradle.properties`. ہمارے حال میں، پلاگ‑این`jbake.ghpages`اسے ایک خاصیت پڑھنا پڑتا تھا`site_config_path`صحیح طریقے سے کام کرنے کے لیے. یونٹ ٹیسٹ کام کے لیے`initialize`اس کو اس خاصیت کے موجود ہونے پر پلاگائن کے سلوک کا جائزہ لینا چاہیے۔

بنیادی مسئلہ یہ ہے کہ یونٹ ٹیسٹس، ڈیزائن کے طور پر، انہیں معزول ہونا چاہیے۔ استعمال کا`ProjectBuilder`گریڈیل ایک انåsٹنس بناتا ہے`Project`یاداشت میں، فائل سسٹم پر ایک حقیقی پراجیکٹ سے مکمل طور پر الگ کردہ۔ اس لئے، یہ ٹیسٹ انسبینس خود بخود فائل نہیں پڑھتی`gradle.properties`اور اس لئے ملکیت کے بارے میں معلومات نہیں ہے`site_config_path`.

مسئلہ کی معماری

درج ذیل ڈائگرام ٹیسٹ ماحول اور فائل سسٹم کے درمیان انقطاع کو دکھاتا ہے:

@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

غلط اچھی خیال کی غوا

ایک پہلا طریقہ فائل بنانے کا ہو سکتا ہے`gradle.properties`test کے وسائل میں مصنوعی۔

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

تاہم، یہ طریقہ ناکام ہونے کے لیے عزم کردہ ہے کیونکہ`ProjectBuilder`یہ فائل نظام کنفیگریشن فائلز کے لئے اسکن کرنے کے لیے ڈیزائن نہیں کیا گیا ہے. ٹیسٹ الگ رہتا ہوگا اور اس فائل کو نظرانداز کرے گا۔

حل : اپراپرٹی سیمیولیٹ کریں `ExtraPropertiesExtension

معقول حل یہ مسئلے کے لیے lire نہیں، بلکہ simuler کرنا ہے تاکہ خاصیت کی موجودگی آبجیکٹ میں direct طور پر دکھائی جائے۔`Project`ٹیسٹ کا Gradle اس کے لیے ایک طاقتور مکانیزم فراہم کرتا ہے : اضافی خصوصیات (Extra Properties).

مرحلہ 1 : ExtraPropertiesExtension کو سمجھنا

`ExtraPropertiesExtension`ایک کلی-مقدار کنٹرینر ہے جو ہر Gradle ماڈل آبجیکٹ سے منسلک ہوتا ہے۔ یہ پروجیکٹ، ٹاسک، یا کسی بھی دیگر Gradle امتدادات پر ڈائنامک کے ساتھ خاصیتیں شامل کرنے کی اجازت دیتا ہے۔

@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

مرحلہ 2 : ٹیسٹ سٹاپ - تیاری

آئیے بنیادی ڈھانچہ بنانا شروع کریں:

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

مرحلہ 3 : پراپرٹی کی انجیکشن

یہ ٹیسٹ پروเจکٹ میں خاصیت انجیکٹ کرنے کا تفصیلی عمل ہے:

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

مرحلہ 4 : حل کے فلوز ڈائراگرام

@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

مرحلہ 5 : پہلے/بعد کا موازنہ

@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: مختلف ٹیسٹ کیسز کا انتظام

مختلف scenario جانچنے کے لیے، مختلف تنظیموں کے ساتھ کئی ٹیسٹ بنائیں :

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

حل کی فائنل آرکیٹیکچر

@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

اس طریقے کے فائدے

  • مکمل عزلتٹیسٹ کسی بھی باہر کی فائل پر منحصر نہیں۔ یہ خود سراہی اور مستقل ہے اور کسی بھی ماحول (لوکل، سی‌آئی/سی ڈی، وغیرہ) میں قابل اعتماد طور پر چلا سکتا ہے۔

  • وضاحت اور نیتٹیسٹ واضح طور پر اپنی تشغيل کے پیشِ ضروری شرط کا اظہار کرتا ہے۔ جس شخص بھی ٹیسٹ پڑھتا ہے، فوراً دیکھتا ہے کہ پلاگ‑إن特性 (خاصیت) کی ضرورت ہے`site_config_path`چلنے کے لیے۔

  • قابلِ نگهداریاگر خاصیت کا نام تبدیل ہو جائے، تو صرف ٹیسٹ میں ایک جگہ پر اس کو اپ ڈیٹ کرنا پڑتا ہے، تجاویز کردہ ٹیسٹ تنظیم فائلیں تبدیل کرنے کی ضرورت نہیں۔

  • لمبائیتہر ٹیسٹ میں inject کردہ property کی قیمت سادگی سے تبدیل کرنے سے مختلف scenario`s کو ٹیسٹ کرنا trivial بن جاتا ہے۔ (مقدر، غیر معتبر، غائب وغیرہ۔)

  • عملکردگیکسی فائل پڑھنا نہیں، سب کچھ میموری میں ہوتا ہے، جو ٹیسٹس کو زیادہ تیز بناتا ہے

  • قابلِ تجدید: ٹیسٹس دترمنسٹ ہیں کیونکہ وہ فائل سسٹم کی حالت پر منحصر نہیں ہیں۔

اچھی عمل اور مشورے

ایک فائدہ مند طریقہ بنائیں

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

خصوصیات کی تصدیق

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
}

نتیجہ

لڑنا بدل سے یونٹ ٹسٹ ماحول کو کنفیگریشن فائلیں پڑھنے پر مجبور کرنے کے بجائے، بہترین عمل مطلوبہ حالت کی نقلید کرنا ہے۔ استعمال`ExtraPropertiesExtension`گriڈل کے خصوصیات کو برنامَج طور پر مقرر کرنے کا طریقہ مؤثر اور قابل اعتماد پلاگ یونٹ ٹیسٹ کرنے کا سب سے صاف اور مضبوط طریقہ ہے۔

یہ تکنیک بیرونی انحصار کے مسئلے کو سادہ انحصار انjekشن (dependency injection) میں تبدیل کر دیتی ہے، جو ٹیسٹ‑ڈریون ڈویلپمنٹ (TDD) کی فلسفے کے دل میں واقع ہے۔ یہ ٹیسٹ کے ماحول پر مکمل کنٹرول فراہم کرتی ہے، جبکہ یونٹ ٹیسٹس کے معیار کے لیے ضروری انکلیڈگی (isolation) کو برقرار رکھتی ہے۔

اس مضامین میں پیش کردہ ڈائیگرامات اور مثالیں دکھاتی ہیں کہ یہ رویہ تدريجي اور منظم طریقے سے امل میں لایا جا سکتا ہے، جس سے آپ اپنا Gradle پلاگائن کے لیے مضبوط اور برقرار رکھنے والی ٹیسٹ سiri بن سکتا ہے۔

متعلقہ مضامین