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`テスト。 Gradle はこれを行うための強力なメカニズムを提供しています: 追加プロパティ (追加プロパティ)。
ステップ 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`動作するために。
-
保守性: プロパティの名前が変更された場合、テストの設定ファイルを操作することなく、テスト内の1か所だけを更新すればよい。
-
柔軟性各テストで注入されたプロパティの値を単純に変更するだけで、さまざまなシナリオをテストすることが容易になります(有効な値、無効な値、存在しない値など)。
-
パフォーマンス: ファイルの読み込みは行われず、すべてメモリ内で行われるため、テストがより高速になります。
-
再現性テストは決定論的であり、ファイルシステムの状態に依存しないからです。
ベストプラクティスとアドバイス
ユーティリティメソッドを作成する
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