問題: 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: テストのセットアップ - 準備

まず、私たちの単体テストの基本構造を作成しましょう :

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é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: 複数のテストケースの管理

異なるシナリオをテストするために、様々な構成を持つ複数のテストを作成しましょう:

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 "テスト環境" {
  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か所だけを更新すればよい。

  • 柔軟性各テストで注入されたプロパティの値を単純に変更するだけで、さまざまなシナリオをテストすることが容易になります(有効な値、無効な値、存在しない値など)。

  • パフォーマンス: ファイルの読み込みは行われず、すべてメモリ内で行われるため、テストがより高速になります。

  • 再現性テストは決定論的であり、ファイルシステムの状態に依存しないからです。

ベストプラクティスとアドバイス

ユーティリティメソッドを作成する

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`Gradleのプロパティをプログラムで設定することは、効果的かつ信頼性の高いプラグインのユニットテストを実現するための最もシンプルで堅牢な方法です。

この技術は、外部依存の問題を単純な依存注入に変換し、テスト駆動開発(TDD)の哲学の核となる部分に組み込む。これにより、テスト環境に対する完全な制御が可能になり、品質の高い単体テストに必要な分離を維持しつつ、効果的なテストを実現できる。

本記事に掲載されている図と例は、このアプローチが段階的かつ体系的にどのように実装できるかを示しており、Gradle プラグインのための堅牢で保守性の高いテストスイートを作成することを可能にします。

関連記事