Testeando tus aplicaciones Java con Spock: tests más expresivos, fáciles de leer y mantener

No es ninguna sorpresa que en el planeta Java la herramienta de test más usada sea JUnit si tomamos en cuenta que fue creada hace más de quince años. Esto, no obstante, no implica que sea la mejor, sino muchas veces proseguimos usando exactamente las mismas herramientas por inercia o bien pues acá siempre y en todo momento se ha hecho esto de esta forma sin proponernos si existen opciones alternativas mejores. Una de estas opciones alternativas es Spock.

Spock es un framework de tests basado en Groovy que podemos usar para testar tanto aplicaciones Java como Groovy. Con Spock podemos redactar tests muy expresivos, simples de leer y sostener. Todo ello es posible por 2 motivos principalmente: el espléndido DSL que da Spock y la potencia de Groovy, lenguaje con el que escribimos los tests.

Da un runner de JUnit con lo que es compatible con cualquier herramienta, IDE y servidor de integración continua que usemos hoy día con JUnit. Por el hecho de que, no nos engañemos, si bien conocemos la relevancia de redactar tests, a todos nos da vagancia escribirlos a veces, y si encima las herramientas no asisten, tenemos un motivo más para no hacerlo. En el presente artículo vamos a ver lo que Spock puede hacer por nuestros tests.

Empezando con Spock

Comenzar a redactar tests con Spock es sencillísimo, lo único que debemos hacer es incorporar a nuestro proyecto Java la dependencia testCompile 'org.spockframework:spock-core:1.0-groovy-dos.4' y, opcionalmente, testCompile 'org.codehaus.groovy:groovy-all:2.4.7' si deseamos emplear una versión de Groovy diferente a la dos.4.1 incluída con Spock.

Creando nuestro primer test

Crearemos nuestro primer test y explicar cada parte en detalle:

import spock.lang.Specification

class MiPrimerTest extends Specification undefined

Con un primer vistazo seguro que nos llaman la atención múltiples unas partes del test:

Todos nuestros tests deben heredar de spock.lang.Specification pues en esa clase se halla definido el runner compatible con JUnit.
El nombre de cada test se escribe como una cadena de texto en comillas. Se terminó el nombrar los test como testInvertirCadenaDeTexto o bien afín. Ahora podemos redactar nombres de tests muy gráficos y que verdaderamente expresan el motivo del test.
En su forma más general todos y cada uno de los tests de Spock se fundamentan en los bloques given, when, then, siendo given en el que establecemos el estado inicial de nuestro test.
En el bloque when describimos los estímulos, esto es, lo que deseamos probar.
Por último, en la parte then vamos a poner las condiciones que se deben cumplir a fin de que el test pase. Es esencial apreciar que lo que escribamos en este bloque son aseveraciones, con lo que no es preciso emplear assert delante de cada una de ellas.

Cabe asimismo resaltar que podemos redactar opcionalmente un pequeño texto explicativo en todas y cada una de los bloques, given, when, then. En verdad se considera una buena práctica pues hace que podamos leer todo el test sin mirar en detalle el código del mismo.

Una opción alternativa a redactar el test precedente que podemos emplear a veces para reducir la elocuencia del mismo es emplear el bloque expect en el que de forma directa definimos nuestras esperanzas.

void 'invertir una cadena de texto'() undefined

¿Y si falla un test?

Si pensamos en TDD, sabemos que el ciclo de test sería redactar un test que falle, redactar el código mínimo que hace que el test pase y por último refactorizar el código.

Una característica que debe tener un buen framework de test es enseñar información relevante de de qué forma y por qué razón ha fallado un test. A ninguno nos agrada tener que ocupar el código de println's o bien tener que depurar cuando el test falla. ¿No sería mejor que el framework nos mostrara de una manera visual y directa por qué razón el test ha fallado? ¡Power asserts al rescate!

Imaginemos el próximo test. Deseamos revisar que el primer lenguaje de un usuario es Java. Este test falla por el hecho de que como vemos, tenemos una lista de lenguajes y el primero de ellos es Groovy.

void 'El nombre del primer lenguaje es Groovy'() undefined

Si ejecutamos el test, aparte de apuntarnos que ha fallado, Spock nos va a enseñar la próxima salida:

Condition not satisfied:

info.lenguajes.nombre.first() == 'Java'
| | | | |
| | | Groovy false
| | [Groovy, Java] cinco differences (dieciseis por ciento similarity)
| | (Groo)v(y)
| | (Ja–)v(a)
| [[nombre:Groovy, conocimientos:10], [nombre:Java, conocimientos:9]]
[nombre:Iván, lenguajes:[[nombre:Groovy, conocimientos:10], [nombre:Java, conocimientos:9]]]

Expected :Java

Actual :Groovy

Vemos que tenemos la información de cada variable del assert de tal modo que podemos ver meridianamente todos y cada uno de los valores y saber por qué razón ha fallado el test sin precisar depurar ni de println's auxiliares.

Una de las killer features de Spock: Data driven testing

En multitud de ocasiones debemos probar exactamente el mismo código mas con diferentes datos de entrada. Además de esto, a veces, el setup preciso para probar el test no es abominable con lo que en la práctica terminamos con un sinnúmero de tests en los que el noventa por ciento del código es exactamente el mismo y solo cambian los datos y el resultado. Para solventar este inconveniente podemos utilizar lo que Spock llama Data driven testing.

void 'comprobando el máximo entre 2 números'() resultado
1

Creo que el test es suficientemente explicativo por sí solo. Hemos creado una tabla de datos y Spock ejecutará el test 3 veces reemplazando en todos y cada una de ellas las variables a, b y resultado con los valores de cada línea. Esta aproximación, si bien directa y muy visual, tiene un pequeño inconveniente. Si cualquiera de las iteraciones hace fallar el test, solo vamos a saber que el test ha fallado mas no vamos a poder saber precisamente como de ellas lo ha hecho fallar. Para solventarlo, agregaremos la anotación @Unroll y además de esto vamos a poder substituir el nombre de las variables en el propio nombre del test.

@Unroll
void 'El máximo entre 2 números #a y #b es #resultado'() cinco
-1

De esta manera, el resultado ahora será:

Aprovechando la potencia de Groovy

Hasta el momento hemos visto ciertas primordiales peculiaridades de Spock mas hay otra que es posible que hayamos pasado de largo: Groovy. Con independencia de que estemos testando código Java o bien Groovy nuestros tests se escriben siempre y en todo momento en Groovy.

Si bien a priori no pueda parecer esencial, o bien aun creas que no deseas aprender un lenguaje nuevo para redactar los tests, en escaso tiempo comprenderás por qué razón es tan esencial Groovy para lograr esa expresión y legibilidad en nuestros tests.
Como creo que la mejor manera de mostrarlo es con un caso. Supongamos que tenemos el próximo procedimiento que deseamos probar que sencillamente devuelve una lista de personas.

public static List makePersonList() undefined

Siendo Person el próximo POJO:

public class Person undefined

Ahora podemos redactar el próximo test:

void 'testeando una lista de personas'() undefined

¿Vemos algo extraño en personList.name? ¿Qué es lo que significa esto? ¿De veras tenemos un List y estamos accediendo a name en esa lista? Realmente lo que ocurre es que estamos aprovechándonos del syntactic sugar que agrega Groovy para poder extraer el nombre de cada entre las personas que están en la lista. El código precedente lo podríamos redactar asimismo de las próximas formas, siendo cada una de ellas un tanto más similar a Java, hasta llegar a la última que sería el equivalente en Java 8:

personList.name
personList*.name // spread operator
personList.collect undefined
personList.collect undefined
personList.collect undefined
personList.stream().map undefined.collect(Collectors.toList())
personList.stream().map undefined.collect(Collectors.toList())

Como veis, por el mero hecho de emplear Groovy para nuestros test hemos logrado estos sean muy expresivos y simples de comprender.

Testeando código con colaboradores

En nuestro código del cada día tenemos clases y objetos que interactúan entre ellos, se llaman entre sí y son dependencias unos de otros. El inconveniente