แก้ไขความท้าทายในการทดสอบหน่วย Gradle ด้วย `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 : อ่านเมื่อเริ่มต้น
JbakePlugin --> GradleProperties : เข้าถึงผ่าน 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 : ใช้
}
note right of ProjectTest : การแยก = \nไม่สามารถเข้าถึงระบบไฟล์
@enduml
ความอยากที่จะเชื่อว่าอะไรเป็นความคิดที่ดี แม้มันเป็นความคิดที่ผิด
วิธีการเริ่มต้นอาจเป็นการสร้างไฟล์`gradle.properties`เทียมในทรัพยากรของการทดสอบ.
// src/test/resources/gradle.properties
site_config_path=src/jbake/settings/site.yml
ยางไรก็ตาม, วิธีนี้ถูกกำหนดให้ล้มเหลว เพราะว่า`ProjectBuilder`ไม่ได้ถูกออกแบบมาเพื่อสแกนระบบไฟล์เพื่อค้นหาไฟล์การกำหนดค่า การทดสอบจะยังคงแยกตัวอยู่และจะเพิกเฉยต่อไฟล์นี้
ทางแก้ปัญหา : จำลองคุณสมบัติกับ `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 : ประกอบด้วย
ExtensionContainer --> ExtraPropertiesExtension : จัดการ
ExtraPropertiesExtension --> Project : ซิงโครไนซ์กับ\nproject.properties
note right of ExtraPropertiesExtension : จุดเริ่มต้นสำหรับ\nฉีดคุณสมบัติ
@enduml
ขั้นตอนที่ 2 : การตั้งค่าการทดสอบ - การเตรียมพร้อม
เริ่มต้นด้วยการสร้างโครงสร้างพื้นฐานของการทดสอบหน่วยของเรา:
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"
)
}
ขั้นตอนที่ 4 : แผนภาพการไหลของโซลูชัน
@startuml
start
:สร้าง ProjectBuilder;
:ได้รับ ExtraPropertiesExtension;
:ฉีดคุณสมบัติ\nผ่าน extra.set();
partition "การตรวจสอบ" {
:ตรวจสอบ project.hasProperty();
:ใช้ปลั๊กอิน;
:ดึงงาน;
}
partition "การทำงาน" {
:เรียกใช้การกระทำ\nของงาน;
:ปลั๊กอิน lit\nproject.properties;
}
partition "การยืนยัน" {
:ตรวจสอบคุณสมบัติ\nเข้าถึงได้;
:ตรวจสอบพฤติกรรม\nที่คาดหวัง;
}
stop
@enduml
ขั้นตอนที่ 5 : การเปรียบเทียบก่อน/หลัง
@startuml
!define RECTANGLE class
package "AVANT - ปัญหา" {
RECTANGLE TestProject1 {
+ ProjectBuilder.build()
+ Pas de propriétés
}
RECTANGLE Plugin1 {
+ initialize()
+ ❌ project.properties["site_config_path"] = null
}
TestProject1 --> Plugin1 : ความล้มเหลว
note right of Plugin1 : การทดสอบล้มเหลวเนื่องจาก\nคุณสมบัติไม่พบ
}
package "APRÈS - วิธีแก้ปัญหา" {
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 : กำหนดค่า
ExtraProps --> Plugin2 : อสังหาริมทรัพย์ที่ว่าง
note right of Plugin2 : ทดสอบสำเร็จ เนื่องจาก\nคุณสมบัติถูกจำลอง
}
@enduml
ขั้นตอนที่ 6: การจัดการกรณีทดสอบหลายชุด
เพื่อทดสอบสถานการณ์ที่แตกต่างกัน ให้เราสร้างหลาย ๆ การทดสอบด้วยการกำหนดค่าที่หลากหลาย:
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 : ใช้
ProjectBuilder --> TestProject : สร้าง
TestProject --> ExtraPropertiesExtension : มี
TestClass --> ExtraPropertiesExtension : ตั้งค่า
ExtraPropertiesExtension --> TestProject : ซิงค์คุณสมบัติ
TestProject --> JbakePlugin : นำไปใช้
JbakePlugin --> TestProject : lit properties
note right of ExtraPropertiesExtension : จุดตรวจสอบ\nเพื่อการฉีด
note bottom of TestProject : สภาพแวดล้อม\nที่ควบคุมและแยกออก
@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 ของคุณ