Resolver el desafío de las pruebas unitarias de Gradle con `gradle.properties
Publié le 13 July 2025
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:
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.
Paso 2: Configuración de la prueba - Preparación
Comencemos por crear la estructura básica de nuestra prueba unitaria :
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:
@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
Paso 5 : Comparación Antes/Después
Paso 6 : Gestión de los casos de prueba multiples
Para probar diferentes escenarios, creemos varias pruebas con configuraciones variadas :
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
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
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
@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.