Groovy y el desarrollo orientado a pruebas

Personalmente llevaba mucho tiempo dándole vueltas a los problemas intrínsecos de un desarrollo orientado a pruebas.

Siempre que he tratado este tema, suele salir en la conversación la dificultad de mantener el código de pruebas, el tiempo que se consume en el desarrollo de las clases de Test, la tendencia que aparece en los proyectos a discontinuar la batería de pruebas según avanza el desarrollo del proyecto.

Bajo este contexto, y desde el conocimiento de las bondades que te permite un lenguaje dinámico orientado a objetos, como puede ser el caso de Groovy, siempre se me ha venido a la cabeza, ¿por qué no incluir el desarrollo de pruebas con Groovy, y aprovechar el potencial del lenguaje para solventar algunos de los problemas que aparecen en los desarrollos orientados a pruebas?

Después de valorarlo, me he puesto manos a la obra y os puedo anticipar que la alternativa cumplió con todas las expectativas que había depositado en ella.

¿En qué nos ayuda Groovy?

  1. Aumento de la productividad debido a que se desarrolla mayor rapidez y mayor sencillez.
  2. Groovy usa el soporte de Junit, con lo que no es necesario introducir una dependencia nueva.
  3. Groovy tienen un conjunto de clases mejoradas (propias de la GDK), entre la que cabe destacar un conjunto de nuevos métodos de aserción.
  4. Groovy presenta soporte intrínseco para stubs y mocks que simplifican la posibilidad de realizar test unitarios, simulando el comportamiento de las clases colaboradoras.
  5. Groovy presenta soporte para ser ejecutado los tests directamente desde Ant, Maven y los entornos de desarrollo integrado.

Un ejemplo :

Veamos todos los conceptos presentados anteriormente en un ejemplo que intenta ser lo más sencillo posible y permite descubrir las ventajas anteriormente descritas.

Lo primero que es necesario conocer, es que las clases de pruebas deben de extender de la clase GroovyTestCase.

class ExampleTest extends GroovyTestCase

Para definir un método de prueba simplemente es necesario definir un método de la siguiente manera:

def testExample(){
assertEquals "Prueba de mensaje", 1+1, 2
}

Como se puede ver en el código tenemos varias ventajas sobre la solución Java:

  • Las colecciones presentan un método grep, al que se le puede pasar una Closure (bloque anónimo de código Groovy), la cual nos permite centrar exclusivamente en la lógica de negocio “clave” dentro de nuestro método (que consiste básicamente en saber si un elemento de la lista es mayor que el target establecido como parámetro). Finalmente tal como se ha definido la semántica del método se devuelve el tamaño de lista resultante (número de elementos de la colección que es mayor que el target).
  • Únicamente una línea de código para implementar la lógica.
  • Las ventajas propias del lenguaje dinámico:
    • Ausencia del tipo devuelto por el método
    • Ausencia de “;” al final de cada línea
    • Métodos de ayuda para el tratamiento de colecciones que nos permite centrarnos en la lógica del método y no en la lógica de iteración, búsqueda de elementos, etc…

Una vez establecido el método que queremos probar pasemos a ver la clase de prueba correspondiente. Antes de nada, nos creamos un método de apoyo privado que permita chequear la aserción oportuna (en este caso de igualdad) dada una lista, un target y un resultado.

private check(expected, list, target){
assertEquals expected, pruebas.mayoresQue(list, target)
}

Comencemos a probar el método:

def testMayoresQue(){
 check 1, NEG_NUMBERS, -3
 check 2, POS_NUMBERS, 2
 check 1, MIXED_NUMBERS, 1
}

Donde:

static final NEG_NUMBERS = [-2,-3,-4], POS_NUMBERS = [2,3,4], MIXED_NUMBERS = [-2, 1, 3, 0]

Cómo se puede ver, la prueba la centramos en probar que funciona con lista de números enteros negativos, positivos, y mixtos.

Si contamos las líneas de código empezamos a ver las ventajas de Groovy:

  • Método mayoresQue: 1 línea
  • Método check de apoyo a la pruebas: 1 línea
  • Método de prueba con 3 escenarios distintos: 3 líneas.

Es cierto que el ejemplo es muy sencillo, pero quizás la mayor diferencia con Java sería la implementación del método mayoresQue.

¿Qué ocurriría en Java si quisiéramos probar el método para cadenas de caracteres?. ¿Qué pasará con Groovy?.

void testMayoresQueAplicadosAString(){
 check 1, ['Hola', 'Pepe', 'Fede'] , 'Huevo'
}

Aquí casi mejor no hago ninguna reseña al respecto. Creo que el ejemplo evidencia la potencia de Groovy en cuanto a tratamiento dinámico de datos.

Una vez identificado un ejemplo sencillo de test, pasemos a revisar el soporte que presenta Groovy para pruebas unitarias.

Para la prueba del soporte de Stub y Mocks hemos establecido dos clases de ejemplo, las cuales depende una de la otra. Veamos la primera de ellas:

class ManipulaCaracteres2 {
 enum Modo{MAYUSCULAS, MINUSCULAS};
 Modo mode;
 def ManipulaCaracteres2(Modo mode) {
 this.mode = mode
}
 String convertir(String cadena, int indice){
 return (mode == Modo.MAYUSCULAS? cadena.substring(0,indice).toUpperCase():cadena.substring(0,indice).toLowerCase()) + cadena.substring(indice)
 }

Se puede ver que la clase permite cambiar una cadena a mayúsculas y a minúsculas desde el inicio hasta el carácter de la posición marcada por el parámetro índice.

Así por ejemplo si llamamos al método

new ManipulaCaracteres2(ManipulaCaracteres2.Modo.MINUSCULAS).convertir(“ESTO_ES_UNA_CADENA_MAYUSCULA”, 4)

Nos dará el resultado: “esto_ES_UNA_CADENA_MAYUSCULA”.

Adicionalmente tenemos una clase que presenta una dependencia con la clase ManipulaCaracteres2:

class ManipulaCaracteres {
 static convertirYduplicarCadena(String cadena, Integer index){
 def mc2 =  new ManipulaCaracteres2(ManipulaCaracteres2.Modo.MAYUSCULAS);
 def cadenaMayusculas = mc2.convertir(cadena, index)
 return cadenaMayusculas + cadenaMayusculas
 }

Básicamente esta clase realiza una llamada al método convertir de la clase ManipulaCaracteres2 y la cadena resultante la concatena dos veces.

Pasemos a probar ambas clases. Para las pruebas de ManipulaCaracteres2 no es necesario introducir ninguna técnica de stub o mocking puesto que no presenta dependencias con clases distintas a las de la GDK. Hacemos un método de prueba para probar la clase con paso a mayúsculas y a minúsculas:

def testManipulaCaracteres2(){
 def x = new ManipulaCaracteres2(ManipulaCaracteres2.Modo.MAYUSCULAS)
 assertEquals x.convertir('pruebaCadena', 6) , 'PRUEBACadena'
 def y = new ManipulaCaracteres2(ManipulaCaracteres2.Modo.MINUSCULAS)
 assertEquals y.convertir('PRUEBACadena', 6) , 'pruebaCadena'
 }

Pasemos a probar la clase ManipulaCaracteres usando la técnica de stub:

def testManipulaCaracteresStub(){
 def stubManipulaCaracteres2 = new StubFor(ManipulaCaracteres2)
 stubManipulaCaracteres2.demand.convertir() {
  cadena, index -> assert (cadena.size() == 3);
  return cadena.toUpperCase()
  }
 stubManipulaCaracteres2.use {
  assertEquals ManipulaCaracteres.convertirYduplicarCadena("pep", 4), "PEPPEP"
  }
 stubManipulaCaracteres2.expect.verify()
}

Dentro del código podemos resaltar lo siguiente:

Lo primero que creamos en la prueba es un stub de la clase con la que mantiene una dependencia la clase ManipulaCaracteres.

def stubManipulaCaracteres2 = new StubFor(ManipulaCaracteres2)

Después mediante la llamada al método demand, procedemos a implementar mediante una closure el comportamiento simulado del método convertir. Dentro de la closure, como se puede ver incluso se pueden realizar aserciones sobre los parámetros de entrada (en este caso que la cadena sea de tamaño 3, esta condición es puramente ilustrativo de las posibilidades que presenta el soporte de Groovy para stubs). Dentro de la closure, por tanto, el desarrollador está centrado en proveer lógica de simulación del comportamiento del método convertir de una manera muy elegante y sencilla.

stubManipulaCaracteres2.demand.convertir() {
 cadena, index -> assert (cadena.size() == 3);
 return cadena.toUpperCase()
}

Finalmente se llama al método use, que recibe una closure igualmente y donde nos centramos en realizar la prueba sobre la clase ManipulaCaracteres, en la cual ya se ha “inyectado” una dependencia de ManipulaCaracteres2 simulada (stub).

stubManipulaCaracteres2.use {
 assertEquals ManipulaCaracteres.convertirYduplicarCadena("pep", 4), "PEPPEP"
}

Finalmente se llama al método verify() que comprobará que todas las aserciones establecidas en la closure asociadas a use se cumplen.

stubManipulaCaracteres2.expect.verify()

Cambiemos ahora la implementación del método convertir e introduzcamos una llamada a un método (sin ninguna semántica concreta). El objetivo es comprobar la semántica del componente stub con Groovy.

String convertir(String cadena, int indice){
 this.prueba();
 return (mode == Modo.MAYUSCULAS? cadena.substring(0,indice).toUpperCase():cadena.substring(0,indice).toLowerCase()) + cadena.substring(indice)
}

Cuando ejecutamos la clase de prueba, nos presenta el siguiente error:

junit.framework.AssertionFailedError: No more calls to 'prueba' expected at this point. End of demands.

Lo cual nos da a indicar que hay una llamada al método prueba dentro de la clase que no estamos controlando desde la clase de test. Por tanto añadimos una llamada al método demand sobre el stub para indicarle a la clase, que también es llamado el método prueba().

void testManipulaCaracteresStub(){
 def stubManipulaCaracteres2 = new StubFor(ManipulaCaracteres2)
 stubManipulaCaracteres2.demand.convertir() {
  cadena, index -> assert (cadena.size() == 3);
  return cadena.toUpperCase()
 }
 stubManipulaCaracteres2.demand.prueba(){
 }
 stubManipulaCaracteres2.use {
  assertEquals ManipulaCaracteres.convertirYduplicarCadena("pep", 4), "PEPPEP"
 }
 stubManipulaCaracteres2.expect.verify()
}

Finalmente pasamos la prueba. Otra modificación que introducimos en el código es la de añadir una llamada adicional al método convertir dentro del método convertirYduplicarCadena (como siempre con carácter ilustrativo y sin ningún significado concreto).

static convertirYduplicarCadena(String cadena, Integer index){
 def mc2 =  new ManipulaCaracteres2(ManipulaCaracteres2.Modo.MAYUSCULAS);
 def cadenaMayusculas = mc2.convertir(cadena, index)
 def cadena2 = mc2.convertir(cadena, index)
 return cadenaMayusculas + cadenaMayusculas
}

Al ejecutar el método de pruebas:

junit.framework.AssertionFailedError: No more calls to 'convertir' expected at this point. End of demands

Ahora comprobamos otra funcionalidad que añade el soporte de stub de Groovy que es la posibilidad de introducir un rango dentro de la llamada a demand de la siguiente manera:

stubManipulaCaracteres2.demand.convertir(2..2) {
 cadena, index -> assert (cadena.size() == 3);
 return cadena.toUpperCase()
}

Ahora bien, si cambiásemos las líneas de demand aplicadas a los métodos prueba y convertir, la ejecución de la llamada al test seguiría siendo satisfactoria. En definitiva, la diferencia entre un stub y un mock reside, en que éste último, es “estricto” en el orden de las sentencias demand, puesto que se tienen que ejecutar en el mismo orden que son declaradas en el método a probar. Así, finalmente la prueba realizada con tecnología Mock sería:

void testManipularCaracteresMock(){
 def mock = new MockFor(ManipulaCaracteres2)
 mock.demand.convertir(2..2) { cadena, index -> "PEPE"}
 mock.demand.prueba(){}
 mock.use {assertEquals ManipulaCaracteres.convertirYduplicarCadena("pepe", 4), "PEPEPEPE"}
}

La llamada a convertir está antes que la llamada a prueba. En el caso que cambiásemos el orden, la ejecución del test nos diría:

No call to ‘convertir’ expected at this point. Still 1 call(s) to ‘prueba’ expected

Como se puede comprobar en el ejemplo, la simulación del método convertir ha sido cambiada por una implementación más sencilla (siempre devuelve “PEPE”).

Conclusión:

Como al inicio del post comenté, hacía bastante tiempo que llevaba detrás de valorar la posibilidad de implementación de tests con un lenguaje dinámico; y mi opinión después de realizar la oportuna prospección, es que aplica 100%, y muchos de los problemas que se disponen normalmente con el desarrollo orientado a pruebas, se pueden ver reducido con el uso de Groovy. Como algunas veces he comentado, Groovy te permite tener una evolución natural para los programadores Java, puesto que al inicio, un programador Java puede empezar con quitar los “;” del final e ir evolucionando en Groovy según va teniendo más experiencia, sin sufrir una pérdida de rendimiento en la migración (cada vez harás código más fino con Groovy). Siguiendo esta línea argumental, ¿por qué no empezar tus primeros experimentos con Groovy en el soporte a las pruebas de tu software? El soporte desde luego es excelente.

Con este post no quiero simplificar, para nada, el problema de la inclusión de pruebas en un proceso de desarrollo, soy consciente de que supone un cambio cultural en la forma de trabajar de los desarrolladores, un cambio fundamental en entender un proceso de desarrollo: desde la venta, hasta el despliegue de la aplicación, pasando por los aspectos más metodológicos, pero ¿quién dijo que la calidad es gratuita?

En el siguiente post intentaremos ahondar en la ejecución de pruebas en cada una de las capas de una arquitectura web, usando como base Groovy&Grails.

That’s all folks!

Federico Caro es un consultor senior con más de 10 años en el desarrollo de aplicaciones/sistemas basados en tecnología J2EE. Su carrera profesional en J2EE comenzó en Peoplecall una empresa dedicada a servicios de telefonía IP, colaborando en el desarrollo de la migración del site de PHP a J2EE integrando los servicios de facturación. Posteriormente ha pasado por departamentos de definición de arquitecturas J2EE en Indra Sistemas, Ralia Technologies (Grupo Damm) e IT-Deusto.

Ver toda la actividad de Federico Caro

Recibe más artículos como este

Recibirás un email por cada nuevo artículo.. Acepto los términos legales

Posts relacionados

Escribe un comentario