Jorge
Posted on March 14, 2022
Seguro que ya lo sabes, pero por si acaso, Gradle es una herramienta para construir artefactos de software. Es una alternativa al archifamoso Maven que en lugar de basarse en una definición de las tareas en XML, com hace este, ha desarrollado su propio DSL siendo el fichero build.gradle
el "punto de entrada" donde se definen las tareas, dependencias, etc.
En Gradle una configuration
es un conjunto de artefactos junto con sus dependencias. Así por ejemplo, en un proyecto típico de Java, Gradle ofrece de por sí dos configuraciones, main
y test
, representadas cada una por sus respectivos directorios.
Así cuando ubicamos un fichero .java en el directorio src/main/java
estamos asignandolo a la configuración main
, por lo que se usarán las dependencias configuradas en main
sobre él, mientras que si ubicamos el fichero en el directorio test
se aplicarán otras dependencias.
Esto es útil para poder indicar de forma sencilla qué dependencias van en cada configuration
. Por ejemplo, las dependencias anotadas con testImplementation
no se utilizan en construir los artefactos ubicados en main
Lo bueno de Gradle es que podemos extender este modelo para incluir nuevas configuraciones de una forma sencilla.
Objetivo
Queremos incluir en nuestro proyecto un nuevo tipo de Tests con unas necesidades específicas y queremos que se integren dentro del resto de tareas pudiendo definir sus depencias, etc.
Por necesidades específicas puedes entender cualquier cosa: unos tests que sólo se pueden ejecutar en un entorno dockerizado concreto, con unas variables de entorno particulares, requieren un software instalado previamente, etc
La solución habitual es etiquetarlos con algún tipo de condición de tal forma que si no se cumplen los requisitos, el test se ignore, pero al final terminamos teniendo una mezcla de tests que dificulta su ejecución e incluso depuración.
En este ejemplo lo que vamos a crear es una configuración específica para testear una imagen nativa creada por el proyecto usando GraalVM. Estos tests van a ejecutar la aplicación nativa y comprobar su funcionamiento como si fueran un usuario ejecutandola desde consola
nativeCliTest
Como ya hemos dicho, vamos a ubicar los test en un nuevo directorio, a la misma altura que el directorio test
(y main
) . Una vez terminada la configuración, nuestro editor lo va a reconocer como uno más.
Diagram
Build.gradle
Para incluir este nuevo directorio como una nueva configuración en gradle, modificaremos el build.gradle
def nativeCliTest = sourceSets.create('nativeCliTest')
configurations[nativeCliTest.implementationConfigurationName].extendsFrom(configurations.testImplementation)
configurations[nativeCliTest.runtimeOnlyConfigurationName].extendsFrom(configurations.testRuntimeOnly)
Con estas líneas estamos creando una nueva configuración igual a la de test
dependencies{
api '....'
implementation '....'
testImplementation '.....'
nativeCliTestImplementation project
nativeCliTestImplementation "org.codehaus.groovy:groovy-sql:3.0.9"
}
Le indicamos a gradle que los artefactos bajo la nueva configuración requieren de las dependencias del proyecto.
Si para estos tipos de test requerimos alguna dependencia extra podemos indicarla como en el ejemplo (groovy-sql) y así no estaremos "contaminando" a las otras configuraciones (es decir, en este ejemplo los artefactos demain
ni de test
no ven esta nueva dependencia)
def nativeCliTestTask = tasks.register('nativeCliTest', Test) {
description = 'Runs nativeCli tests.'
group = 'verification'
useJUnitPlatform()
testClassesDirs = nativeCliTest.output.classesDirs
classpath = configurations[nativeCliTest.runtimeClasspathConfigurationName] + nativeCliTest.output
dependsOn nativeCompile
environment 'NATIVE_BINARY_PATH', "$buildDir/native/nativeCompile/miaplicacion"
}
tasks.named('check') {
dependsOn(nativeCliTestTask)
}
Creamos una nueva tarea nativeCliTestTask
(o como quieras llamarla), que extiende Test
y la configuramos para que use las dependencias de nativeCliTest
Así mismo la incluimos dentro de las tareas necesarias para completar la tarea check
, es decir, cuando le indiquemos a Gradle que queremos ejecutar la tarea check
, gradle ejecutará :
compileXXXX
build
test
integrationTest (si lo hubiera)
nativeCompile
nativeCliTestTask
check
Test
Con esta configuración cualquier test que creemos en la carpeta nativeCliTest
dispondrá de una variable de entorno NATIVE_BINARY_PATH
con la ruta a la imagen nativa generada previamente
- TIP
-
Obviamente este es mi caso particular. En el tuyo lo que hacen particulares a estos test puede ser cualquier otra variable de entorno, ejecutable, etc
import groovy.sql.Sql
import org.testcontainers.containers.MySQLContainer
import spock.lang.Specification
import spock.lang.Timeout
class MysqlTest extends Specification {
static MySQLContainer container
static {
container = new MySQLContainer("mysql:5.6")
container.start()
}
@Timeout(30)
def 'should run native binary' () {
given:
def BIN = System.getenv('NATIVE_BINARY_PATH')
def CLI = [BIN,
'-u', container.username,
'-p', container.password,
'--url', container.jdbcUrl,
'--driver', 'com.mysql.cj.jdbc.Driver',
'--dialect', 'mysql'
]
when:
def proc = new ProcessBuilder()
.command(CLI)
.redirectErrorStream(true)
.start()
and:
def result = proc.waitFor()
then:
result == 0
and:
Sql.newInstance(container.jdbcUrl, container.username, container.password)
.rows("SELECT * FROM mytable")
}
}
IDE
(Al menos con Intellij) Al configurar un nuevo sourceSet el IDE es capaz de reconecer qué tipo es y qué dependencias necesita, etc y nos permite ejecutar y depurar cada test de forma independiente si lo desamos
Conclusion
En este post me he centrado en añadir un nuevo tipo de test al estilo de integrationTest
o functionalTest
pero puedes extenderlo a cualquier otro tipo de artefactos que requieran unas dependencias especiales
Posted on March 14, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.