حل چالش تستهای واحد گرادل با `gradle.properties
منتشر شده در 13 July 2025
مشکل: عزل تستهای واحد از یک پلاگین Gradle
در هنگام نوشتن تستهای واحد برای یک پلاگین Gradle، یک چالش رایج مدیریت وابستگیهای پیکربندی پروژه، مانند ویژگیهایی که در فایل تعریف شدهاند، است.gradle.properties. در حالت ما، پلاگین`jbake.ghpages`باید یک خاصیت را بخواند`site_config_path`برای کارکردن صحیح. تست واحد برای وظیفه`initialize`باید رفتار پلاگین را در وجود این ویژگی بررسی کند.
مشکل اساسی این است که تستهای واحدی، به صورت طراحی، باید منفصل باشند. استفاده از`ProjectBuilder`Gradle یک نمونه از`Project`در حافظه، بهطور کامل از یک پروژه واقعی روی سیستم فایلها جداست. در نتیجه، این نمونه تست به صورت خودکار فایل را نمیخواند.gradle.properties`و بنابراین اطلاع ندارد از ملک`site_config_path.
معمار مسئله
نمودار زیر جدایی بین محیط تست و سیستم فایل را نشان میدهد :
@startuml
!define RECTANGLE class
package "محیط تولید" {
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 "محیط تست" {
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`مصنوعی در منابع تست.
// src/test/resources/gradle.properties
site_config_path=src/jbake/settings/site.yml
اما، این روش محکوم به شکست است چون`ProjectBuilder`برای اسکن سیستم فایلها به دنبال فایلهای پیکربندی طراحی نشده است. تست در isolation باقی میماند و این فایل را نادیده میگیرد.
حل : شبیهسازی ویژگی با `ExtraPropertiesExtension
راهحل شایسته برای این مشکل این نیست که خواندن فایل باشد، بلکه شبیهسازی وجود خصوصیت را مستقیماً در آبجیک انجام دهیم`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
مرحله ۲ : تنظیم تست - آمادهسازی
ابتدا ساختار اصلی تست واحد خود را ایجاد کنیم:
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: تزریق ویژگی
اینجا فرآیند دقیق برای تزریق ویژگی در پروژه تست است:
@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"
)
}
مرحله ۴: نمودار جریان حل
@startuml
start
:Créer ProjectBuilder;
:Obtenir ExtraPropertiesExtension;
:Injecter la propriété\nvia extra.set();
partition "بررسیها" {
:Vérifier project.hasProperty();
:Appliquer le plugin;
:Récupérer la tâche;
}
partition "اجرا" {
:Exécuter les actions\nde la tâche;
:Le plugin lit\nproject.properties;
}
partition "ادعاهای" {
:Vérifier la propriété\nest accessible;
:Valider le comportement\nattendues;
}
stop
@enduml
مرحله ۵ : مقایسه قبل/بعد
@startuml
!define RECTANGLE class
package "پیش - مسأله" {
RECTANGLE TestProject1 {
+ ProjectBuilder.build()
+ Pas de propriétés
}
RECTANGLE Plugin1 {
+ initialize()
+ ❌ project.properties["مسیر_پیکربندی_سایت"] = null
}
TestProject1 --> Plugin1 : échec
note right of Plugin1 : Test échoue car\nla propriété n'existe pas
}
package "پس - راهحل" {
RECTANGLE TestProject2 {
+ ProjectBuilder.build()
+ ExtraPropertiesExtension
}
RECTANGLE ExtraProps {
+ set("مسیر_پیکربندی_سایت", "...")
}
RECTANGLE Plugin2 {
+ initialize()
+ ✅ project.properties["مسیر_پیکربندی_سایت"] = "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
مرحله ۶ : مدیریت موارد آزمایش متعدد
برای آزمایش سناریوهای مختلف، چندین تست با تنظیمات مختلف ایجاد کنیم :
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 "محیط تست" {
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
فواید این رویکرد
-
عزل کامل: تست به هیچ فایل خارجی وابسته نیست. خودمختار است و میتواند بهطور قابل اعتماد در هر محیطی (محلی، CI/CD و غیره) اجرا شود.
-
وضوح و نیت: تست صریحاً پیشنیازهای اجرا را اعلام میکند. هر کس که تست را بخواند، بلافاصله میبیند که پلاگین نیاز به خاصیت دارد.`site_config_path`برای کارکردن.
-
قابلیت نگهداریاگر نام ویژگی تغییر کرد، کافی است آن را در یک مکان فقط در تست بهروزرسانی کنید، بدون نیاز به دستکاری فایلهای پیکربندی تست.
-
لچک: تست سناریوهای مختلف به سادگی امکانپذیر میشود با تغییر ساده مقدار ویژگی تزریقشده در هر تست (مقدار معتبر، نامعتبر، غایب و غیره).
-
عملکرد: هیچ خواندن فایلی نیست؛ تمام کارها در حافظه انجام میشود که این کار تستها را سریعتر میکند.
-
قابلیت بازتولیدتستها قطعی هستند زیرا به وضعیت سیستم فایل وابسته نیستند.
بهترین شیوهها و توصیهها
ایجاد یک متد کمکی
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
}
}
}
اعتبارسنجی ویژگیها
@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`تعریف به صورت برنامهای ویژگیهای Gradle، روش تمیزترین و قویترین برای انجام تستهای واحد مؤثر و قابل اعتماد برای افزونهها است.
این تکنیک یک مشکل وابستگی خارجی را به یک تزریق وابستگی ساده تبدیل میکند که در قلب همان فلسفه Test-Driven Development (TDD) است. این تکنیک کنترل کامل بر محیط تست را فراهم میکند، در حالی که جدایی لازم برای تستهای واحد با کیفیت را نیز حفظ میکند.
دیاگرامها و نمونههای ارائهشده در این مقاله نشان میدهد که این رویکرد چگونه میتواند بهصورت تدریجی و منهجی پیادهسازی شود؛ این امکان را میدهد یک بسته تست قوی و پایدار برای پلاگینهای Gradle خود ایجاد کنید.