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 : 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`설계된 것이 파일 시스템을 스캔하여 구성 파일을 찾는 것이 아니므로, 테스트는 격리된 상태로 남아 이 파일을 무시합니다.
해결책 : `ExtraPropertiesExtension`를 사용해 속성을 시뮬레이션하기
이 문제의 우아한 솔루션은 파일을 읽는 것이 아니라, 객체에 속성의 존재를 직접 시뮬레이션*하는 것이다.`Project`de 테스트. 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단계: 테스트 설정 - 준비
먼저, 우리 단위 테스트의 기본 구조를 만들어봅시다 :
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
: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
단계 5: 비교 전/후
@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
단계 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 : 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 속성을 정의하는 것이 효율적이고 신뢰할 수 있는 플러그인 단위 테스트를 수행하는 가장 깔끔하고 견고한 방법입니다.
이 기술은 외부 의존성 문제를 간단한 의존성 주입으로 변환하여 테스트 주도 개발(TDD)의 철학 그 자체에 심어줍니다. 이 기술은 테스트 환경에 대한 완전한 제어를 제공하면서도 고품질 단위 테스트에 필요한 격리를 유지합니다.
이 기사에 제시된 다이어그램과 예시는 이 접근 방식을 점진적이고 체계적으로 구현하는 방법을 보여주며, 이를 통해 Gradle 플러그인을 위한 견고하고 유지 관리 가능한 테스트 제품군을 만들 수 있습니다.
관련 기사
31 May 2026
14 May 2026