Test unitarios con Groovy y Spock
Veamos algunos ejemplos de cómo hacer test unitarios con Groovy y Spock Framework. Basado en JUnit, Spock nos permite hacer test unitarios de nuestro código Java, Groovy o cualquier otro lenguaje de la máquina virtual Java con una sintaxis simple, legible y muy similar a la sintaxis de los test BDD (Behavior Driven Development).
Dependencias maven/gradle[editar]
Si usamos maven las dependencias de test a poner serían
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.10</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.1-groovy-2.4-rc-3</version>
<scope>test</scope>
</dependency>
Aparte, la configuración que sea necesaria para que nuestro proyecto maven trabaje con Groovy
Si usamos gradle, las dependencias serían
dependencies { compile 'org.codehaus.groovy:groovy-all:2.4.10' testCompile 'org.spockframework:spock-core:1.1-groovy-2.4-rc-3' }
y por supuesto, un apply plugin: 'groovy'
para que nuestros test se ejecuten en las fases normales de construcción de gradle.
en ambos casos, la dependencia de JUnit viene incluida con la dependencia de spock-core y la dependencia de groovy-all debe ser de compile o de test según nuestro código principal esté o no en Groovy.
Con todo esto, podemos poner nuestros test de Groovy y Spock en el directorio src/test/groovy
para que se ejecuten.
Test básico con Spock[editar]
Cogemos la clase Java Stack
y le hacemos un test básico, para ver cómo funciona esto. Simplemente, le hacemos push()
de un entero y comprobamos que pop()
nos devuelve dicho entero.
El código Groovy con el test puede ser así
package com.chuidiang.examples
import spock.lang.Specification
class StackTest extends Specification{
def "push and pop single element"() {
given:
Stack stack = new Stack()
when:
stack.push(11)
then:
stack.pop() == 11
stack.size() == 0
}
}
La clase de test hereda de Specification
, una clase propia de Spock.
El método de test se suele poner entre comillas, usando espacios y los caracteres que queramos, de forma que sea legible. En nuestro caso, lo hemos puesto en inglés, para llevar la contraria : "push and pop single element"()
Dentro del test, tres bloques: given:
, when: y then:
. Veamos qué ponemos en cada uno de ellos
given:
: Creamoa e inicializamos si es necesario las clases que nos hagan falta para el test. En este caso, sólo hacemos una instancia de la claseStack
de Java.when:
: Aquí hacemos las llamadas que queramos a nuestras clases bajo test para ver si funcionan bien. En nuestro caso, sólo añadimos un 11 a la pila.then:
: Aquí ponemos las condiciones que deben cumplirse después de la ejecución de nuestro código enwhen:
. Verificamos en este caso que la llamada astack.pop()
nos devuelve el 11 que acabamos de meter y que después de haberlo extraido, el tamaño de la pila queda a cero. No es necesario ponerassert
típico de Groovy. Por estar en el bloquethen:
, Spock ya sabe que debe dar el fallo si no se cumple la condición.
Como se puede ver, queda bastante legible (a pesar del inglés).
A veces no es necesario o no queda muy claro el tener un when:
y un then:
separados. Por ejemplo, imagina que tienes una clase Calculator
con un método add(int a, int b)
que devuelve la suma. Si ponemos when:
y then:, tenemos dos posibles opciones, porque la tercera no compila siquiera.
// Opcion 1, usar una variable intermedia
when:
def sum = calculator.add (1,2)
then:
sum == 3
// Opcion 2, dejar vacío el when
when:
then:
calculator.add(1,2) == 3
// Opcion 3, da error al compilar, no poner when
Pues bien, hay una forma más elegante que es reemplazar ambas etiquetas por una sola etiqueta expect:
def "1+1=2" () {
expect:
calculator.add(1,1) == 2
}
Poner texto explicativo[editar]
Todas las etiquetas que hemos visto admiten texto detrás, entre comillas, a modo de comentario. Adicionalmente, podemos añadir tantas etiquetas and:
como queramos para separar el código de las anteriores etiquetas, y podemos poner también texto en esta etiqueta and:
. Por ejemplo
def "un test cualquiera"() {
given: "un primer objeto de flores"
def flores = new Flor()
and: "un segundo objeto con agua"
def regadera = new Regadera()
when: "riego las flores"
regadera.applyTo(flores)
and: "pongo las flores al sol"
flor.toSol()
then: "Las flores crecen"
flor.grownUp() == true
and: "son bonitas"
flor.isBeautiful() == true
}
Básicamente es como añadir comentarios de código, pero quizás quedan más claros al tener un formato similar al de la estructura de etiquetas.
setup y cleanup del test[editar]
Si dentro de la clase tenemos varios test, es posible que en los given:
se nos repita el mismo código una y otra vez. Por ejemplo, si hacemos varios test de Stack
dentro de esta clase, necesitaremos hacer el new Stack()
en cada método.
Spock permite que pongamos atributos a la clase de test, pero de forma que cada test instancia de nuevo esos atributos. Por ejemplo, si hacemos
package com.chuidiang.examples
import spock.lang.Specification
/**
* Created by JAVIER on 25/04/2017.
*/
class StackTest extends Specification{
Stack stack = new Stack()
def "push and pop single element"() {
when:
stack.push(11)
then:
11==stack.pop()
0 == stack.size()
}
def "push two elements, pop last added"() {
when:
stack.push(11)
stack.push(12)
then:
12 == stack.pop()
1 == stack.size()
}
}
Hemos hecho dos métodos de test, pero hemos quitado el bloque given:
. En su lugar, hemos creado un atributo de la clase Stack stack = new Stack()
. Pues bien, esa pila es la que tendrán disponibles los dos métodos de test, pero Spock se encargará de crear una pila nueva para cada test, de forma que no influya en un test el cómo haya dejado la pila el test anterior.
Si el Stack
fuese un objeto costoso de construir y no nos importara compartirlo entre los distintos métodos de test, podemos añadirle la anotación @Shared
. De esta forma se instancia una única vez y es compartido por todos los métodos de la clase.
class StackTest extends Specification{
@Shared
Stack stack = new Stack()
Si necesitásemos código adicional para realizar antes de cada test, igual para todos, podemos definir el método setup()
y meter ahí el código que queramos que se ejecute antes de cada test.
class StackTest extends Specification{
@Shared
Stack stack = new Stack()
def setup(){
// Este codigo se ejecuta antes de cada metodo de test
println 'setup'
}
Y si adicionalmente cada método de test necesitara su propia inicialización, podemos volver a añadir la etiqueta given:
class StackTest extends Specification{
@Shared
Stack stack = new Stack()
def setup(){
// Este codigo se ejecuta antes de cada metodo de test
println 'setup'
}
def "push and pop single element"() {
given:
println 'given'
De la misma forma que setup
, cleanup
permite limpiar o liberar lo que creamos necesario después de cada test. Si cada test tiene código específico que ejecutar para limpiar, se puede poner la etiqueta cleanup:
def "push and pop single element"() {
given:
println 'given label'
when:
stack.push(11)
then:
11==stack.pop()
0 == stack.size()
cleanup:
println 'cleanup label'
}
Si hubiese un código de limpieza final común a todos los test, se puede poner en un método cleanup
, tal que así
class StackTest extends Specification{
@Shared
Stack stack = new Stack()
def cleanup() {
stack.clear()
}
setupSpec() y cleanupSpec()[editar]
Hemos visto que @Shared
nos permite instanciar una sola instancia que será compartida por todos los test de nuestra clase de test. Si quisieramos código que se ejecutara una única vez antes de que se ejecuten los test, podemos declarar un método setupSpec()
. De igual manera, si definimos un método cleanupSpec()
, se ejecutará una sola vez después de que todos los métodos de test terminen.
class StackTest extends Specification{
@Shared
Stack stack = new Stack()
def setupSpec() {
// Este codigo se ejecuta una sola vez antes de que se ejecute
// ningun metodo de test
println "setupSpec"
}
def cleanupSpec() {
// Este codigo se ejecuta una sola vez despues de que todos
// los metodos de test terminen
println "cleanupSpec"
}
Etiqueta where: para tablas de datos[editar]
A veces necesitamos ejecutar un mismo test varias veces, usando cada vez un conjunto diferente de datos. La etiqueta where:
de Spock nos ayuda con este asunto. Basta ponerla al final y definir ahí una tabla de datos, como en el siguiente ejemplo
class CalculatorTest extends Specification {
Calculator calculator = new Calculator()
def "a + b = c"() {
expect:
calculator.add(a,b) == c
where:
a|b||c
1|1||2
1|2||3
2|1||3
}
}
Hemos usado las variables a
, b
y c
para los sumandos y el resultado. En la etiqueta where:
definimos la tabla. En la primera línea, separados por |
ponemos los nombres de las variables que estamos usando. En las siguientes líneas, también separados por |
los conjuntos de valores para el test. Cada fila hará que se ejecute una vez el test con los valores de esa fila concreta. La separación con doble ||
es opcional, se podría usar igualmente un solo |
, pero se suele hacer así para que visualmente queden más claras las entradas del test (a la izquierda del doble ||
) y las salidas esperadas, a la derecha.
También es posible esta otra definición, totalmente equivalente
def "otro a + b = c"() {
expect:
calculator.add(a,b) == c
where:
a << [1,2,1]
b << [1,1,2]
c << [2,3,3]
}
Nuevamente las tres variables y el operador <<
para ir metiendo los valores de los arrays que hay detrás uno en cada ejecución del test.
Si estos test fallan, el nombre del test que falla "a + b = c" no nos dice nada. Podemos hacerlo más amigable si el nombre del test se mostrara con los valores concretos que estamos probando. A ello nos ayuda la anotación @Unroll
, y poner etiquetas estilo #a
en el nombre del método. Si ejecutamos lo siguiente
@Unroll
def "#a + #b = #c"() {
expect:
calculator.add(a,b) == c
where:
a|b||c
1|1||2
1|2||3
2|1||4 // Aqui hemos metido un fallo a posta
}
Al ejecutar, obtendremos una salida como la siguiente
com.chuidiang.examples.CalculatorTest > 2 + 1 = 4 FAILED org.spockframework.runtime.SpockComparisonFailure at CalculatorTest.groovy:15
pero si no hubieramos puesto el @Unroll
ni los caracteres #
en el nombre del método, la salida sería más confusa
com.chuidiang.examples.CalculatorTest > #a + #b = #c FAILED org.spockframework.runtime.SpockComparisonFailure at CalculatorTest.groovy:15
Mock Objects[editar]
Spock tiene soporte para mock object. Cuando quieres testear una clase, posiblemente esa clase interactúa con otras clases y necesitas instanciarlas también para poder hacer tu test. Pero instanciar las clases reales puede tener dos problemas. Por un lado, pueden ser difíciles de instanciar, imagina que esa clase accede a una base de datos, o establece conexión con un web service, o cualquier otra cosa que se te ocurra. Para que esa clase funcione, necesitarías tener la base de datos también, o el web service, o lo que sea. Por otro lado, en tu test no tienes control de que hace la clase bajo test con esas otras clases.
La solución para esto son los mock object. Un mock object es una clase que te haces específica para el test y que tiene la misma interfaz que esa otra clase que necesitas instanciar. A tu clase bajo test le pasas este mock object para que se crea que es el objeto real, pero tú tienes control sobre él.
Veamos cómo se hace esto con Spock. Imagina que vamos a guardar nuevos usuarios en una base de datos. Tenemos una interfaz IfzDataBase
con un par de métodos para verificar si un usuario ya existe en base de datos y para insertar un nuevo usuario
interface IfzDataBase {
void addUser (String user, String password)
boolean userExists (String user)
}
En algún sitio, y no nos interesa para nuestro test, habrá una clase que implemente esta interfaz y que sea la que realmente haga esas consultas e inserciones en una base de datos real.
Por otro lado, imagina que tenemos la clase UserManagment
a la que se le pide que dé de alta un nuevo usuario pasando su nombre y dos veces la nueva password para ese usuario, tal cual la suelen pedir los interfaces de usuario donde se registran usuarios: un nombre y la password repetida dos veces para confirmar que no te has equivocado al escribirla. La clase puede ser como esta.
class UserManagment {
// Instancia de la clase que accede a base de datos.
IfzDataBase dataBase
// Añade nuevo usuario, verificando que nombre y password no son null ni vacios, que el usuario no existe
// y que las passwords coinciden.
def addUser (String name, String password, String rePassword) throws UserExistsException, PasswordMismatchException{
assert name // Ni nulo, ni vacio.
assert password
if (dataBase.userExists(name)){
throw new UserExistsException(name)
}
if (password!=rePassword){
throw new PasswordsDoesnotMatch()
}
dataBase.addUser(name,password)
}
}
No vamos a entrar en detalles de cómo está implementada. Simplemente hay que fijarse que recibe tres parámetros: name
, password
y rePassword
y que lanza un par de excepciones UserExistsException
y PasswordMismatchException
en caso de que el usuario ya exista o que las password no coincidan. Esta es la clase de la que queremos hacer un test. El código de test con Spock puede ser como el siguiente
class UserManagmentTest extends Specification{
IfzDataBase dataBase = Mock()
UserManagment userMangment = new UserManagment(dataBase: dataBase)
def "add valid user"(){
when:
userMangment.addUser("Pedro","secret","secret")
then:
1*dataBase.userExists("Pedro")
1*dataBase.addUser("Pedro","secret")
}
Veamos algunos detalles. Primero se crean las instancias necesarias de las clases. Como de IfzDataBase
tenemos la interfaz y no queremos hacer el test contra una base de datos real, modificando su contenido y haciendo más lento y pesado el test, instanciamos un mock object de IfzDataBase
. Basta con hacer la llamada Mock()
para obtener la instancia. Spock viendo que la asignamos a un dato de tipo IfzDataBase
, sabe qué interfaz tiene que implementar el mock object.
Luego instanciamos nuestra clase bajo test UserManagment
pasándole en el constructor nuestro mock object. Y listo, ahora solo nos queda trabajar con ello. En la parte when:
añadimos un usuario con su datos válidos, y en la parte then:
verificamos:
- Se ha llamado una vez al método
dataBase.userExists()
pasando como parámetro "Pedro" - Se ha llamado una vez al método
dataBase.addUser()
pasando como parámetros el nombre de usuario y la password.
El número de veces que se llama a un método puede ser un número, un rango y el carácter _
hace de "comodín". Por ejemplo
1 * dataBase.userExists("Pedro")
Se tiene que haber llamado al método exactamente una vez.0 * dataBase.userExists("Pedro")
No se tiene que haber llamado a ese método.(1..3) * dataBase.userExists("Pedro")
Se tiene que haber llamado a ese método entre 1 y 3 veces.(1.._) * dataBase.userExists("Pedro")
Se tiene que haber llamado a ese método al menos una vez.(_..3) * dataBase.userExists("Pedro")
Se tiene que haber llamado a ese método como máximo tres veces._ * ataBase.userExists("Pedro")
Se tiene que haber llamado a ese método cualquier número de veces, incluido cero veces.
También podemos poner condiciones más elaboradas en los parámetros. Por ejemplo
1 * dataBase.userExists("Pedro")
Se le llama una vez con parámetro "Pedro"1 * dataBase.userExists(!"Pedro")
Se le llama una vez con parámetro que no sea "Pedro"1 * dataBase.userExists()
Se le llama sin parámetros.1 * dataBase.userExists(_)
Se le llama con un parámetro cualquiera, incluido null1 * dataBase.userExists(*_)
Se le llama, con o sin parámetros1 * dataBase.userExists(!null)
Se le llama con un parámetro que no sea null1 * dataBase.userExists(_ as String)
Se le llama con cualquier parámetro que sea String no null1 * dataBase.userExists({ it.size() > 3 })
Una closure en la que podemos poner la condición que queramos. En este ejemplo, que el argumento tenga una longitud mayor de 3
Y también podemos, si hay métodos de nombre parecido pero que nos vale cualquiera de ellos, usar una expresión regular para el nombre del método, algo como esto
1 * dataBase./user.*/("Pedro")
Llamada a cualquier método que empiece por user
Dónde definir las interacciones del Mock[editar]
Las interacciones del Mock son las líneas en las que decimos cuántas llamadas esperamos a cada método. En el ejemplo las hemos puesto en el then:
, pero pueden ponerse también el a declaración del Mock si son comunes para todos los test. La forma de hacerlo sería así
IfzDataBase dataBase = Mock() {
1*userExists("Pedro")
1*userExists("Pedro","password")
}
pero lo dicho, esto vale si en todos nuestros test esperamos estas llamadas.
Valores de retorno del Mock : Stubbing[editar]
En nuestro ejemplo la interfaz IfzDataBase
tiene un método userExists()
que devuelve un boolean true o false, según el usuario ya exista o no en base de datos. Un objeto Mock por defecto devuelve valores como null, false, 0, ... . Si en nuestro test necesitáramos que devolviera otro valor, no tenemos más que indicarlo. Por ejemplo, la siguiente declaración de Mock devuelve true en el método userExists()
si se le pasa "Juan" como parámetro
class UserManagmentTest extends Specification{
IfzDataBase dataBase = Mock() {
userExists("Juan") >> true
}
Así, podemos hacer el siguiente test
def "reject already existing user"() {
when:
userMangment.addUser ("Juan", "password", "password")
then:
thrown(UserAlreadyExistsException.class)
0*dataBase.addUser(_,_)
}
Se intenta crear un usuario "Juan" y verificamos que salta la excepción UserAlreadyExistsException
y que no se ha llamado al método addUser()
OJO, CUIDADO, ATENCION : Hay un detalle importante a tener en cuenta aquí. Las verificaciones que pongamos en el then:
relativas al Mock, sobre escriben el comportamiento predefinido el Mock. Si estamos tentados de comprobar que se ha llamado al método userExists()
de esta forma
def "reject already existing user"() {
when:
userMangment.addUser ("Juan", "password", "password")
then:
thrown(UserAlreadyExistsException.class)
1*dataBase.userExists("Juan") // Pretendemos verificar que se ha llamado al método.
0*dataBase.addUser(_,_)
}
el test dará fallo, no lanzará la excepción y sí llamará al addUser
. El problema es la línea 1*dataBase.userExists("Juan")
que sobre escribe el comportamiento deseado para ese método cuando se le pasa "Juan" y devuelve el valor por defecto false. Si queremos hacer esta comprobación, debemos indicar qué valor debe devolver el método userExists()
de esta forma
def "reject already existing user"() {
when:
userMangment.addUser ("Juan", "password", "password")
then:
thrown(UserAlreadyExistsException.class)
1*dataBase.userExists("Juan") >> true // Forma correcta de hacerlo.
0*dataBase.addUser(_,_)
}
Si no necesitamos en ningún test saber cuántas veces se llama a los métodos, sino que sólo necesitamos que el objeto Mock devuelva valores para poder ejecutar el test, entonces podemos crear un objeto Stub
en vez de un objeto Mock
. Las reglas son las mismas, pero no podemos hacer comprobaciones de cuántas veces se ha llamado al método o con qué parámetro.
class UserManagmentTest extends Specification{
IfzDataBase dataBase = Stub(){
userExists("Juan") >> true
}
def "reject already existing user"() {
when:
userMangment.addUser ("Juan", "password", "password")
then:
thrown(UserAlradyExistsException.class)
}
}
Con lo visto hasta ahora, cada llamada al método devolverá siempre el mismo valor, dependiendo de los parámetros. A veces puede ser interesante que sucesivas llamadas al método devuelvan valores distintos. Podemos usar la siguiente sintaxis para indicar la lista de valores a devolver
IfzDataBase dataBase = Stub(){
userExists("Juan") >>> [false, true, true, true]
}
o incluso podemos concatenar listas o meter closures en medio para calcular el valor a devolver
IfzDataBase dataBase = Stub(){
userExists("Juan") >>> [false, true] >> { throw new SQLException() } >> [true, true]
}
En la secuencia anterior, la tercera llamada hará saltar una SQLException
Dónde definir los stubbing[editar]
Podemos definirlos en la creación del objeto Mock, como hemos visto.
También, en cada test, puede definirse en la etiqueta given:
, que es lo adecuado si cada test necesitara sus propios valores de vuelta. Quedaría
def "reject already existing user"() {
given:
dataBase.userExists("Juan") >> true
when:
userMangment.addUser ("Juan", "password", "password")
then:
thrown(UserAlradyExistsException.class)
0*dataBase.addUser(_,_)
}
Finalmente, puede definirse también en la etiqueta then:
, como hemos visto en el ejemplo del apartado anterior, el apartado de Ojo, Cuidado, Atención.
Spies[editar]
No vamos a entrar muy en detalle en los Spy
puesto que la misma documentación de Spock indica que nos pensemos dos veces nuestro diseño si necesitamos usar esta característica.
Un Spy
instancia el objeto real de nuestro código y nos permite interceptar las llamadas, de forma que en nuestro test podamos ver si se está llamando a los métodos, o incluso hacer que el objeto real devuelva una cosa distinta. Cuando llamemos a un método del Spy
, en realidad se acabará llamando al objeto real y devolviendo lo que devuelva el objeto real. Un Spy
se instancia así
def subscriber = Spy(SubscriberImpl, constructorArgs: ["Fred"])
siendo SubscriberImpl
la clase real que queremos instanciar y observar y constructorArgs
un array con los posibles parámetros a pasar al constructor de la clase SubscriberImpl
.
A partir de aquí, en los bloques then:
de nuestro test podemos ver si se han producido llamadas a los métodos de estas clases.
1 * subscriber.receive(_) // Esperamos que haya habido una llamada al metodo receive() con cualquier valor de parámetro.
y para hacer que el método devuelva una cosa distinta de lo que devolvería el método real, con el >>
, como antes
1 * subscriber.receive(_) >> "ok" // Devolverá "ok" en vez de lo que devuelva el método receive()