Test unitarios con Groovy y Spock

De ChuWiki
Saltar a: navegación, buscar

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

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

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:, </code>when:</code> 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 clase Stack 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 en when:. Verificamos en este caso que la llamada a stack.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 poner assert típico de Groovy. Por estar en el bloque then:, 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 </code>then:</code>, 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

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

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()

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

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

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 null
  • 1 * dataBase.userExists(*_) Se le llama, con o sin parámetros
  • 1 * dataBase.userExists(!null) Se le llama con un parámetro que no sea null
  • 1 * dataBase.userExists(_ as String) Se le llama con cualquier parámetro que sea String no null
  • 1 * 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

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

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

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

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()

Enlaces