El problema: aislar las pruebas unitarias de un plugin Gradle

Al escribir pruebas unitarias para un plugin de Gradle, un desafío común es gestionar las dependencias de la configuración del proyecto, como las propiedades definidas en el archivo`gradle.properties`. En nuestro caso, el plugin`jbake.ghpages`debía leer una propiedad`site_config_path`para funcionar correctamente. La prueba unitaria para la tarea`initialize`debía comprobar el comportamiento del plugin en presencia de esta propiedad.

El problema fundamental es que las pruebas unitarias, por diseño, deben ser aislados. El uso de`ProjectBuilder`de Gradle crea una instancia de`Project`en memoria, completamente desconectada de un proyecto real en el sistema de archivos. Por lo tanto, esta instancia de prueba no lee automáticamente el archivo`gradle.properties`y por lo tanto no tiene conocimiento de la propiedad`site_config_path`.

Arquitectura del Problema

El siguiente diagrama ilustra la desconexión entre el entorno de prueba y el sistema de archivos:

test isolation problem

La Tentación de una Falsa Buena Idea

Una primera aproximación podría ser crear un archivo`gradle.properties`ficticio en los recursos de la prueba.

// src/test/resources/gradle.properties
site_config_path=src/jbake/settings/site.yml

Sin embargo, este método está destinado al fracaso porque`ProjectBuilder`no está diseñado para escanear el sistema de archivos en busca de archivos de configuración. La prueba permanecería aislada e ignoraría este archivo.

La Solución : Simular la Propiedad con `ExtraPropertiesExtension

La solución elegante a este problema no es leer el archivo, sino simular la presencia de la propiedad directamente en el objeto`Project`de test. Gradle proporciona un mecanismo poderoso para eso : las propiedades adicionales (Extra Properties).

Paso 1: Comprender ExtraPropertiesExtension

`ExtraPropertiesExtension`es un contenedor clave-valor adjunto a cada objeto del modelo Gradle. Permite agregar propiedades de forma dinámica a un proyecto, una tarea o cualquier otra extensión Gradle.

extra properties architecture

Paso 2: Configuración de la prueba - Preparación

Comencemos por crear la estructura básica de nuestra prueba unitaria :

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...
    }
}

Paso 3: Inyección de la Propiedad

Este es el proceso detallado para inyectar la propiedad en el proyecto de prueba:

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"
    )
}

Paso 4 : Diagrama de flujo de la solución

solution flow

Paso 5 : Comparación Antes/Después

before after comparison

Paso 6 : Gestión de los casos de prueba multiples

Para probar diferentes escenarios, creemos varias pruebas con configuraciones variadas :

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) }
        }
    }
}

Arquitectura Final de la Solución

final architecture

Las ventajas de este enfoque

  • Aislamiento completoEl test no depende de ningún archivo externo. Es autónomo y puede ejecutarse de manera fiable en cualquier entorno (local, CI/CD, etc.).

  • Claridad y intención: El test declara explícitamente los requisitos previos para su ejecución. Cualquiera que lea el test ve inmediatamente que el plugin requiere la propiedad`site_config_path`para funcionar.

  • mantenibilidad: Si el nombre de la propiedad cambia, basta con actualizarlo en un solo lugar en la prueba, sin tener que manipular archivos de configuración de prueba.

  • FlexibilidadResulta trivial probar diferentes escenarios cambiando simplemente el valor de la propiedad inyectada en cada prueba (valor válido, inválido, ausente, etc.).

  • Rendimiento: No se lee ningún archivo, todo ocurre en memoria, lo que hace que las pruebas sean más rápidas.

  • ReproducibilidadLas pruebas son deterministas porque no dependen del estado del sistema de archivos.

Buenas Prácticas y Consejos

Crear un Método Utilitario

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
        }
    }
}

Validación de las Propiedades

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
}

Conclusión

En lugar de luchar por hacer que un entorno de prueba unitaria lea archivos de configuración, la mejor práctica consiste en simular el estado requerido. El uso de`ExtraPropertiesExtension`para definir mediante programación las propiedades de Gradle es el método más limpio y robusto para realizar pruebas unitarias de plugins eficaces y fiables.

Esta técnica transforma un problema de dependencia externa en una simple inyección de dependencia, en el corazón mismo de la filosofía del Desarrollo Dirigido por Pruebas (TDD). Ofrece un control total sobre el entorno de prueba manteniendo el aislamiento necesario para pruebas unitarias de calidad.

Los diagramas y ejemplos presentados en este artículo muestran cómo este enfoque puede llevarse a cabo de manera progresiva y metódica, permitiendo crear una suite de pruebas robusta y mantenible para sus plugins de Gradle.

Articles connexes