6. Framework de test Spock.

En esta sesión veremos en profundidad como comprobar que nuestra aplicación desarrollada en Grails funciona tal y como esperamos y para ello utilizaremos un framework para la realización de tests llamado Spock. Empezaremos viendo conceptos generales tanto de tests como de Spock y terminaremos viendo su aplicación en una aplicación en Grails.

6.1. Introducción a los tests

6.1.1. ¿Qué son los tests?

Definición

Los tests de software se pueden definir como el proceso empleado para comprobar la corrección de un programa informático y se puede considerar como una parte más en el proceso del control de calidad.

6.1.2. Tipos de tests de software

Básicamente existen tres tipos de tests de software:

  • Tests unitarios, que consisten en comprobar el funcionamiento de un determinado módulo sin tener en cuenta a los demás. En este tipo de pruebas tampoco se tiene acceso a recursos como bases de datos, sistemas de ficheros o recursos de red.

  • Tests de integración, suponen un nivel por encima a los tests unitarios y en ellos se añaden recursos como bases de datos, sistemas de ficheros o recursos de red.

  • Tests funcionales, consiste en comprobar que la funcionalidad de nuestra aplicación es la esperada. Básicamente este tipo de pruebas se basan en automatizar las típicas pruebas de rellenar datos en formularios, hacer clicks en botones y analizar la respuesta obtenida.

6.1.3. Otros frameworks de tests

En Java disponemos de varias alternativas para el desarrollo de los tests, pero básicamente disponemos de dos que son las más conocidas y utilizadas:

  • JUnit (http://junit.org), posiblemente sea el framework más extendido entre la comunidad Java.

  • TestNG (http://testng.org), es un framework que permite testear nuestra aplicación con pruebas unitarias, de integración y funcionales.

Sin embargo, en la comunidad Groovy cada vez se está extendiendo más el uso de un framework conocido como Spock (https://code.google.com/p/spock/). Spock surgió en el año 2009 y su creador es Peter Niederwieser (@pniederw) y sin duda alguna destaca entre el resto de frameworks por la expresividad de su lenguaje y su integración con Groovy y Java. Estas son algunas de las razones por las que vamos a utilizar Spock:

  • Fácil de aprender

  • Integración con Groovy

  • Eliminación aserciones innecesarias

  • Información detallada en las salidas de los tests

  • Diseñado desde el punto de vista del usuario

  • Independientemente de la teoria que sigas para desarrollar tus tests (TDD, BDD, etc), Spock se adaptará a tus necesidades

  • Lenguaje altamente expresivo

  • Compatible con JUnit

  • Combina lo mejor de otros conocidos frameworks como JUnit o jMock

6.2. Framework de tests Spock

6.2.1. Instalación en Grails

Spock en Grails viene como un plugin con lo que únicamente tendremos que añadir el plugin en el archivo de configuración BuildConfig.groovy, tal y como se explica en la página del plugin (http://grails.org/plugin/spock) para versiones superiores a Grails 2.2

grails.project.dependency.resolution = {
  repositories {
    grailsCentral()
    mavenCentral()
  }
  dependencies {
    test "org.spockframework:spock-grails-support:0.7-groovy-2.0"
  }
  plugins {
    test(":spock:0.7") {
      exclude "spock-grails-support"
    }
  }
}

Para que el plugin sea instalado y el IDE reconozca las nuevas clases, necesitaremos lanzar algún target de Grails, como por ejemplo grails test-app. Una vez instalado el plugin, ya podremos empezar a desarrollar nuestros primeros tests.

6.2.2. Tests en Spock

Por defecto en Spock, todas las clases que creemos para nuestros tests deben extender la clase spock.lang.Specification, que inyectará una serie de métodos y utilidades en nuestra clase extendida.

A diferencia de otros frameworks de tests, Spock presenta una sintaxis muy sencilla de leer que se basa en la utilización de bloques de código. Este sería un ejemplo básico de test con Spock:

package es.ua.expertojava.todo

import spock.lang.Specification

class MyFirstTest extends Specification {

    def "El operador suma funciona correctamente"() {
        expect:
            10 == 2 + 8
        and:
            10 == 0 + 10
        and:
            10 == 10 + 0
    }
}

Este formato de test en Spock podríamos decir que es el más sencillo de los que podemos encontrar y como puedes observar la sintaxis es muy sencilla de entender. Lo primero que cabe destacar aunque probablemente ya te hayas dado cuenta es el nombre de los métodos que están entrecomillados y podemos llamarlo como queramos y utilizar espacios o caracteres especiales (no ingleses) como acentos.

Destacar también que no es necesario utilizar aserciones dentro del bloque expect. Pero veamos otro ejemplo un poco más elaborado y que nos introducirá un nuevo formato de test en Spock.

def "El método plus de los números funciona correctamente"() {
    when:
        def result = 10.plus(2)
    then:
        result == 12
}

Este formato de spock sea posiblemente el más común en Spock y en él vemos un bloque when y un bloque then. El bloque when se utiliza habitualmente para lanzar el método a probar y en este caso también para almacenar el resultado, mientras que el bloque then se utiliza para comprobar que la respuesta proporcionada por el método es la correcta. De nuevo, en el bloque then no es necesario utilizar aserciones.

Pero, ¿qué pasa si queremos probar varios casos en la misma sentencia? Para este caso, Spock introduce el bloque where en donde con una sintaxis muy sencilla de entender en forma de tabla, vamos a poder comprobar varios al mismo tiempo. Veamos el anterior ejemplo mejorado:

@Unroll
def "El método plus funciona con el sumador #sumador y el sumando #sumando"() {
    when:
        def result = sumador.plus(sumando)
    then:
        result == resultadoEsperado
    where:
        sumador |   sumando     |   resultadoEsperado
        0       |   0           |   0
        0       |   1           |   1
        1       |   1           |   2
        -1      |   0           |   -1
        0       |   -1          |   -1
        -1      |   -1          |   -2
        2       |   1           |   3
}

En este ejemplo vemos la forma en la que Spock crea variables dentro del bloque where sin definir su tipo o ni tan siquiera con la palabra reservada def. De esta forma, en un único test hemos podido comprobar el método plus() con varios valores en una única sentencia.

Por otro lado, vemos también como el nombre del test introduce los valores de las variables definidas dentro del bloque where de tal forma que cuando ejecutemos este test, si alguno falla veremos rápidamente que caso es el problemático.

Además, la anotación @Unroll nos permite que cada uno de los casos en el bloque where se despliegue como un nuevo test, lo cual de nuevo, facilita la detección rápida de los errores. Esta sería la salida producida en nuestro editor sin la anotación @Unroll:

Resultado de los tests sin la anotación @Unroll

y esta es la producida con la anotación @Unroll:

Resultado de los tests con la anotación @Unroll

Como puedes observar, la anotación @Unroll nos proporciona más información del resultado de los tests.

El siguiente ejemplo es idéntico al anterior pero en lugar de utilizar una tabla utilizamos listas con valores:

@Unroll
def "El método plus funciona con el sumador #sumador y el sumando #sumando utilizando listas"() {
    when:
        def result = sumador.plus(sumando)
    then:
        result == resultadoEsperado
    where:
        sumador << [0,0,1,-1,0,-1,2]
        sumando << [0,1,1,0,-1,-1,1]
        resultadoEsperado << [0,1,2,-1,-1,-2,3]
}

El último bloque de un test Spock que nos queda por ver es el primero de todos, el bloque given, que nos servirá para realizar todo lo necesario antes de ejecutar el método a testear. Este podría ser un ejemplo:

def "El método para concatenar cadenas añade un signo + entre las cadenas concatenadas"() {
    given:
        String.metaClass.concat = { String c ->
            "${delegate}+${c}"
        }
    expect:
        "cadena1".concat("cadena2") == "cadena1+cadena2"
    cleanup:
        String.metaClass = null
}

Y como siempre, no olvides devolver la clase metaprogramada con String.metaClass = null.

6.3. Spock en Grails

Una vez hemos visto los primeros ejemplos de tests con Spock en Groovy, vamos a ver ejemplos de como testear los diferentes artefactos en las aplicaciones desarrolladas con Grails. Pero antes de continuar, hay que comentar que en Spock disponemos de unos métodos que se ejecutan antes y después de cada tests o de cada clase Spock. Estos métodos son:

  • setupSpec(): se ejecuta antes del primer test de una determinada clase de tests con Spock

  • cleanupSpec(): se ejecuta después del último test de una determinada clase de tests con Spock

  • setup(): se ejecuta antes de cada test de una determinada clase de tests con Spock

  • setupSpec(): se ejecuta después de cada test de una determinada clase de tests con Spock

6.3.1. Tests con Spock sobre clases de dominio en Grails

Empecemos pues con los tests sobre los artefactos de una aplicación Grails y lo haremos sobre las clases de dominio y más concretamente sobre las restricciones.

Partiendo de nuestra clase de dominio Category,

package es.ua.expertojava.todo

class Category {

    String name
    String description

    static hasMany = [todos:Todo]

    static constraints = {
        name(blank:false)
        description(blank:true, nullable:true, maxSize:1000)
    }

    String toString(){
        name
    }
}

Vamos a comprobar que las restricciones impuestas sobre las propiedades name y description se cumplen. Para ello, vamos a reutilizar un test unitario creado cuando generábamos las clases de dominio llamado CategorySpec.groovy dentro del paquete es.ua.expertojava.todo.

Lo ideal es que cada test unitario compruebe únicamente un aspecto, así que vamos a seguir este consejo y crearemos los siguientes tests:

package es.ua.expertojava.todo

import grails.test.mixin.TestFor
import spock.lang.Specification
import spock.lang.Unroll

/**
 * See the API for {@link grails.test.mixin.domain.DomainClassUnitTestMixin} for usage instructions
 */
@TestFor(Category)
class CategorySpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    def "El nombre de la categoría no puede ser la cadena vacía"() {
        given:
            def c1 = new Category(name:"")
        when:
            c1.validate()
        then:
            c1?.errors['name']
    }

    def "Si el nombre no es la cadena vacía, este campo no dará problemas"() {
        given:
            def c1 = new Category(name:"algo")
        when:
            c1.validate()
        then:
            !c1?.errors['name']
    }

    def "La descripción de la categoría puede ser la cadena vacía"() {
        given:
            def c1 = new Category(description: "")
        when:
            c1.validate()
        then:
            !c1?.errors['description']
    }

    def "Si la descripción es la cadena vacía, este campo no dará problemas"() {
        given:
            def c1 = new Category(description:"")
        when:
            c1.validate()
        then:
            !c1?.errors['description']
    }

    def "La descripción de la categoría puede ser null"() {
        given:
            def c1 = new Category(description: null)
        when:
            c1.validate()
        then:
            !c1?.errors['description']
    }

    @Unroll
    def "Si la descripción tiene menos de 1001 caracteres, no dará problemas"() {
        given:
            def c1 = new Category(description: "a"*characters)
        when:
            c1.validate()
        then:
            !c1?.errors['description']
        where:
            characters << [0,1,999,1000]
    }

    @Unroll
    def "Si la descripción tiene más 1000 caracteres, dará problemas"() {
        given:
            def c1 = new Category(description: "a"*characters)
        when:
            c1.validate()
        then:
            c1?.errors['description']
        where:
            characters << [1001,1002]
    }
}

Con esta batería de tests nos aseguraremos de que las instancias creadas para la clase de dominio Categoría cumplen los requisitos definidos en las restricciones. De igual forma, vamos a realizar un último test para comprobar que cuando se crea una instancia de Categoría correctamente, el valor devuelto por ésta coincide con la sobrecarga del método toString() que hayamos hecho en la clase de dominio.

...
    def "La instancia de Categoría devuelve su nombre por defecto"() {
        expect:
            new Category(name:"The category name").toString() == "The category name"
    }
...

6.3.2. Tests con Spock sobre controladores en Grails

Una vez hemos visto como podemos testear nuestras clases de dominio, vamos a ver como podemos hacer lo mismo con los controladores y para ello vamos a basarnos en el código generado para el controlador CategoryController.groovy. Cuando creábamos este controlador, al mismo tiempo se creaba un test CategoryControllerSpec.groovy con código también generado, pero que nosotros necesitaremos completar, en este caso, con la información necesaria procedente de la clase de dominio Category.

Si abrimos el archivo CategoryControllerSpec.groovy con nuestro editor veremos como en la parte superior hay definido un método con un comentario de tipo TODO para avisarnos de que hay algo que debemos completar, que básicamente será la información necesaria para crear una instancia de tipo Tag de forma correcta.

package es.ua.expertojava.todo

import grails.test.mixin.*
import spock.lang.*

@TestFor(CategoryController)
@Mock(Category)
class CategoryControllerSpec extends Specification {

    def populateValidParams(params) {
        assert params != null
        // TODO: Populate valid properties like...
        //params["name"] = 'someValidName'
    }
...

En nuestro caso, sólo será necesario añadir la información referente al nombre, para que los tests se ejecuten de forma satisfactoria, con lo que podríamos tener algo así:

def populateValidParams(params) {
    assert params != null
    params["name"] = 'Category name'
}

Si ahora ejecutamos en línea de comandos

grails test-app CategoryControllerSpec unit:

comprobaremos como todos los tests se han ejecutado correctamente. Veamos como quedaría el código del test:

package es.ua.expertojava.todo

import grails.test.mixin.*
import spock.lang.*

@TestFor(CategoryController)
@Mock(Category)
class CategoryControllerSpec extends Specification {

    def populateValidParams(params) {
        assert params != null
        params["name"] = 'Category name'
    }

    void "Test the index action returns the correct model"() {

        when:"The index action is executed"
            controller.index()

        then:"The model is correct"
            !model.categoryInstanceList
            model.categoryInstanceCount == 0
    }

    void "Test the create action returns the correct model"() {
        when:"The create action is executed"
            controller.create()

        then:"The model is correctly created"
            model.categoryInstance!= null
    }

    void "Test the save action correctly persists an instance"() {

        when:"The save action is executed with an invalid instance"
            request.contentType = FORM_CONTENT_TYPE
            request.method = 'POST'
            def category = new Category()
            category.validate()
            controller.save(category)

        then:"The create view is rendered again with the correct model"
            model.categoryInstance!= null
            view == 'create'

        when:"The save action is executed with a valid instance"
            response.reset()
            populateValidParams(params)
            category = new Category(params)

            controller.save(category)

        then:"A redirect is issued to the show action"
            response.redirectedUrl == '/category/show/1'
            controller.flash.message != null
            Category.count() == 1
    }

    void "Test that the show action returns the correct model"() {
        when:"The show action is executed with a null domain"
            controller.show(null)

        then:"A 404 error is returned"
            response.status == 404

        when:"A domain instance is passed to the show action"
            populateValidParams(params)
            def category = new Category(params)
            controller.show(category)

        then:"A model is populated containing the domain instance"
            model.categoryInstance == category
    }

    void "Test that the edit action returns the correct model"() {
        when:"The edit action is executed with a null domain"
            controller.edit(null)

        then:"A 404 error is returned"
            response.status == 404

        when:"A domain instance is passed to the edit action"
            populateValidParams(params)
            def category = new Category(params)
            controller.edit(category)

        then:"A model is populated containing the domain instance"
            model.categoryInstance == category
    }

    void "Test the update action performs an update on a valid domain instance"() {
        when:"Update is called for a domain instance that doesn't exist"
            request.contentType = FORM_CONTENT_TYPE
            request.method = 'PUT'
            controller.update(null)

        then:"A 404 error is returned"
            response.redirectedUrl == '/category/index'
            flash.message != null


        when:"An invalid domain instance is passed to the update action"
            response.reset()
            def category = new Category()
            category.validate()
            controller.update(category)

        then:"The edit view is rendered again with the invalid instance"
            view == 'edit'
            model.categoryInstance == category

        when:"A valid domain instance is passed to the update action"
            response.reset()
            populateValidParams(params)
            category = new Category(params).save(flush: true)
            controller.update(category)

        then:"A redirect is issues to the show action"
            response.redirectedUrl == "/category/show/$category.id"
            flash.message != null
    }

    void "Test that the delete action deletes an instance if it exists"() {
        when:"The delete action is called for a null instance"
            request.contentType = FORM_CONTENT_TYPE
            request.method = 'DELETE'
            controller.delete(null)

        then:"A 404 is returned"
            response.redirectedUrl == '/category/index'
            flash.message != null

        when:"A domain instance is created"
            response.reset()
            populateValidParams(params)
            def category = new Category(params).save(flush: true)

        then:"It exists"
            Category.count() == 1

        when:"The domain instance is passed to the delete action"
            controller.delete(category)

        then:"The instance is deleted"
            Category.count() == 0
            response.redirectedUrl == '/category/index'
            flash.message != null
    }
}

¿Qué podemos extraer de estos tests generados?

  • Una serie de variables han sido automáticamente inyectadas en nuestro test como son controller, model, view, response, request y flash.

  • Se utiliza la anotación @Mock() para mockear el comportamiento de la clase de dominio pasada por parámetro. De esta forma se simula su almacenamiento.

  • Para invocar un método cualquiera del controlador simplemente debemos ejecutar controller.method()

  • La variable model nos permitirá comprobar si el modelo que devolvemos es el esperado

  • Las redirecciones se pueden comprobar con la variable response.redirectedUrl

6.3.3. Tests con Spock sobre librerías de etiquetas, vistas y plantillas en Grails

Como siempre, cuando creábamos la librería de etiquetas TodoTagLib.groovy, se creaba automáticamente un esqueleto de test unitario:

package es.ua.expertojava.todo

import grails.test.mixin.TestFor
import spock.lang.Specification

/**
 * See the API for {@link grails.test.mixin.web.GroovyPageUnitTestMixin} for usage instructions
 */
@TestFor(TodoTagLib)
class TodoTagLibSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    void "test something"() {
    }
}

Mediante la anotación @TestFor(TodoTagLib) la variable tagLib se inyecta automáticamente en nuestros tests. Si hacemos memoria, cuando hablábamos de las librerías de etiquetas poníamos como ejemplo

def includeJs = {attrs ->
    out << "<script src='scripts/${attrs['script']}.js'></script>"
}

Vamos a ver a continuación como podemos comprobar que esta etiqueta devuelve lo que pensamos.

void "La etiqueta includeJs devuelve una referencia a la librería javascript pasada por parámetro"() {
    expect:
        applyTemplate('<todo:includeJs script="" />') == "<script src='scripts/.js'></script>"
        applyTemplate('<todo:includeJs script="myfile" />') == "<script src='scripts/myfile.js'></script>"
}

Además de testear las librerías de etiquetas, vamos a comprobar que las vistas y las plantillas devuelven lo que deben devolver. Si recordamos en la sesión de vistas y plantillas, creábamos una plantilla para pintar el pie de página. Vamos a poder comprobar que esta plantilla devuelve lo correcto mediante un test unitario:

void "El pie de página se renderiza correctamente"() {
    when:
        def result = render(template: '/common/footer')

    then:
        result == "<div class=\"footer\" role=\"contentinfo\">\n" +
            "    &copy; 2015 Experto en Desarrollo de Aplicaciones Web con JavaEE y Javascript<br/>\n" +
            "    Aplicación Todo creada por Francisco José García Rico (21.542.334F)\n" +
            "</div>"
}

6.3.4. Tests con Spock sobre servicios en Grails

Tal y como sucede con el resto de artefactos de una aplicación en Grails, cuando creamos un servicio nuevo automáticamente se genera también un test unitario que nos permitirá testear los métodos del servicio.

package es.ua.expertojava.todo

import grails.test.mixin.TestFor
import spock.lang.Specification

/**
 * See the API for {@link grails.test.mixin.services.ServiceUnitTestMixin} for usage instructions
 */
@TestFor(TodoService)
class TodoServiceSpec extends Specification {

    def setup() {
    }

    def cleanup() {
    }

    void "test something"() {
    }
}

En su momento creábamos un método llamado getNextTodos() que nos devolvía las tareas programadas para los próximos días. Para poder testear este método, vamos a necesitar crear una serie de instancias de la clase de dominio Todo. Si recordamos el método en cuestión

def getNextTodos(Integer days, params) {
    Date now = new Date(System.currentTimeMillis())
    Date to = now + days
    Todo.findAllByDateBetween(now, to, params)
}

vemos que se utiliza una llamada a un método dinámico de GORM, con lo que se se realizará una consulta a la base de datos. Pero, si recordamos la definición de tests unitarios que veíamos al inicio de esta sesión, se hacía mención a que los tests unitarios no tienen acceso a recursos externos tales como la base de datos, con lo que, ¿cómo vamos a poder solucionar este problema?

Podríamos optar por dos soluciones. Bien crear un test de integración para tener acceso a la base de datos o bien, mockear la creación de las clases de dominio necesarias. Nosotros optamos por la segunda solución, ya que en términos de velocidad de ejecución de los tests, los tests de integración son considerablemente más lentos que los tests unitarios.

Para mockear la creación de una serie de instancias de una determinada clase de dominio, vamos a utilizar el método llamado mockDomain al cual le podemos pasar que clase de domino queremos mockear y cuales son esas instancias. Con esto podríamos tener un test como éste para comprobar el método getNextTodos():

void "El método getNextTodos devuelve los siguientes todos de los días pasado por parámetro"() {
    given:
        def todoDayBeforeYesterday = new Todo(title:"Todo day before yesterday", date: new Date() - 2 )
        def todoYesterday = new Todo(title:"Todo yesterday", date: new Date() - 1 )
        def todoToday = new Todo(title:"Todo today", date: new Date())
        def todoTomorrow = new Todo(title:"Todo tomorrow", date: new Date() + 1 )
        def todoDayAfterTomorrow = new Todo(title:"Todo day after tomorrow", date: new Date() + 2 )
        def todoDayAfterDayAfterTomorrow = new Todo(title:"Todo day after tomorrow", date: new Date() + 3 )
    and:
        mockDomain(Todo,[todoDayBeforeYesterday, todoYesterday, todoToday, todoTomorrow, todoDayAfterTomorrow, todoDayAfterDayAfterTomorrow])
    and:
        def nextTodos = service.getNextTodos(2,[:])
    expect:
        Todo.count() == 6
    and:
        nextTodos.containsAll([todoTomorrow, todoDayAfterTomorrow])
        nextTodos.size() == 2
    and:
        !nextTodos.contains(todoDayBeforeYesterday)
        !nextTodos.contains(todoToday)
        !nextTodos.contains(todoYesterday)
        !nextTodos.contains(todoDayAfterDayAfterTomorrow)
}

En el test, en primer lugar creamos todas las instancias de la clase de dominio Todo que vamos a utilizar para realizar las comprobaciones. La primera comprobación que realizamos es comprobar que el contador de instancias de la clase Todo es igual al número de elementos creados.

A continuación, ya comprobamos el método getNextTodos() para comprobar que sólo devuelve aquellas instancias creadas que coincidan con el criterio de que sean tareas a realizar en el futuro. Esto significa que sólo las instancias todoTomorrow y todoDayAfterTomorrow serán devueltas.

6.3.5. Tests con Spock mockeando objetos

Los tests unitarios se pueden complicar mucho cuando entran en funcionamiento otros objetos en el método que queremos probar. Por ejemplo, en la sesión sobre vistas y etiquetas planteábamos un ejercicio para escribir una etiqueta que nos imprimiera por pantalla un icono diferente en función de si el valor pasado por parámetro era cierto o falso. Esa etiqueta se aconsejaba utilizar a su vez la etiqueta asset.image() procedente del plugin asset pipeline.

Si pensamos en que test podríamos utilizar para comprobar el funcionamiento de la etiqueta printIconFromBoolean, podríamos pensar algo así:

@Unroll
void "El método printIconFromBoolean devuelve una ruta a una imagen"() {
    when:
        def output = applyTemplate('<todo:printIconFromBoolean value="${value}" />', [value:value])
    then:
        output == expectedOutput
    where:
        value   |   expectedOutput
        true    |   "icontrue.png"
        false   |   "iconfalse.png"
}

Sin embargo, si ejecutamos este test, veremos como el test no se ejecuta correctamente debido a que no sabe como ejecutar un método llamado assetPath. ¿assetPath? ¿De dónde viene la llamada a ese método?

Si analizamos el código del método image de la librería de etiquetas AssetsTagLib veremos como se realiza una llamada al método assetPath que queda fuera del contexto de nuestro test unitario con lo que no podemos acceder directamente a él. Para solucionar este problema, debemos mockear la llamada que nuestra etiqueta printIconFromBoolean realiza al método image() de la librería de etiquetas del plugin asset pipeline. De esta forma, podemos aislar nuestra prueba unitaria de las llamadas realizadas por otros objetos colaboradores y nos centraremos en la lógica de nuestro método.

@Unroll
void "El método printIconFromBoolean devuelve una ruta a una imagen"() {
    given:
        def assetsTagLib = Mock(AssetsTagLib)
        tagLib.metaClass.asset = assetsTagLib
    when:
        def output = applyTemplate('<todo:printIconFromBoolean value="${value}" />', [value:value])
    then:
        output == expectedOutput
    and:
        1 * assetsTagLib.image(_) >> { value ? "icontrue.png" : "iconfalse.png" }
    where:
        value   |   expectedOutput
        true    |   "icontrue.png"
        false   |   "iconfalse.png"
}

En primer lugar, debemos mockear la librería AssetsTagLib para poder devolver lo que nosotros queramos y que no sea necesario invocar al método assetPath. Además, debemos indicar una referencia al namespace asset que nuestra librería TodoTagLib va a utilizar como colaborador. Esto lo hacemos con algo de metaprogramación

tagLib.metaClass.asset = assetTagLib

y básicamente estamos creando una referencia entre el objeto asset en el contexto de la librería de etiquetas TodoTagLib y nuestro objecto mockeado con la librería AssetsTagLib. De esta forma, lo único que nos queda por hacer es sobrecargar el método image().

Para sobrecargar este método utilizamos una técnica que nos permite incluso saber cuantas veces un método ha sido invocado y con que parámetros. Esta técnica se conoce como Tests basados en interacciones.

Para ello lo primero que hemos hecho ha sido generar ese objecto mockeado con

def assetsTagLib = Mock(AssetsTagLib)

Por último, debemos indicar cuantas interacciones vamos a realizar con el método del objeto mockeado y cual va a ser su salida.

1 * assetsTagLib.image(_) >> { value ? "icontrue.png" : "iconfalse.png" }

Con esto estamos indicando que sólo se va a producir una llamada al método image() de la librería de etiquetas AssetsTagLib y que además, esta llamada se puede realizar con cualquier valor como primer (y único) parámetro (esto lo conseguimos utilizando el placeholder _). Si quisiéramos asegurarnos de que los parámetros pasados son los correctos podríamos tener algo así:

1 * assetsTagLib.image([src:expectedOutput]) >> { value ? "icontrue.png" : "iconfalse.png" }

6.3.6. Algunas anotaciones interesantes

Spock presenta una serie de anotaciones que nos servirán de apoyo para realizar nuestros tests.

Ignore

Esta anotación se puede utilizar a nivel de método o a nivel de clase y como su nombre indica sirve para indicar al proceso que ejecuta los tests que el método en cuestión o la clase no deben ser ejecutados.

@Ignore
def "my feature"() { ... }

@Ignore
class MySpec extends Specification { ... }

Incluso podemos pasar como parámetro a la anotación un motivo por el cual este test no va a ser ejecutado.

@Ignore(reason = "TODO")
def "my feature"() { ... }
IgnoreRest

Esta anotación sólo se puede especificar a nivel de método y le indicará al proceso encargado de ejecutar los tests que sólo los métodos que proporcionen esta anotación deben ser ejecutados.

def "I'll be ignored"() { ... }

@IgnoreRest
def "I'll run"() { ... }

def "I'll also be ignored"() { ... }
IgnoreIf

Esta anotación indica que el método anotado sólo se ejecutará si se cumplen una serie de condiciones.

@IgnoreIf({ System.getProperty("os.name").contains("windows") })
def "I'll run everywhere but on Windows"() { ... }

Para facilitar la comprensión de la condición existen una serie de propiedades disponibles dentro del closure:

  • sys Un mapa con todas las propiedades del sistema

  • env Un mapa con todas las variables de entorno

  • os Información acerca del sistema operativo (ver spock.util.environment.OperatingSystem)

  • jvm Información sobre la máquina virtual de Java (ver spock.util.environment.Jvm)

Con lo que el ejemplo anterior puede ser reescrito de la siguiente forma:

@IgnoreIf({ os.windows })
def "I'll run everywhere but on Windows"() { ... }
Requires

Es la anotación contraria a @IgnoreIf. Los métodos anotados con esta anotación sólo se ejecutarán bajo ciertas condiciones.

@Requires({ os.windows })
def "I'll only run on Windows"() { ... }
Stepwise

Es conveniente que los tests no dependan unos de otros para su correcta ejecución, pero en ocasiones por rapidez, necesitaremos ejecutar los tests en un determinado orden. Esto lo podemos conseguir con la anotación @Stepwise que se utiliza a nivel de clase.

Imagina por ejemplo un test funcional con el que pretendemos comprobar que las 4 operaciones básicas sobre las tareas (creación, eliminación, actualización y borrado) se ejecutan correctamente. Lo ideal sería crear 4 tests distintos, uno para cada operación, pero, ¿qué pasaría si pretendemos eliminar una tarea que todavía ni siquiera ha sido creada? En este ejemplo, es necesario que todos los tests se ejecuten en un determinado orden.

Timeout

La anotación @Timeout se puede aplicar a nivel de clase o a nivel de método y nos permitirá especificar un tiempo máximo de ejecución de un test o de una clase de tests. La unidad por defecto será en segundos, pero vamos a poder modificar esta unidad.

@Timeout(5)
def "I fail if I run for more than five seconds"() { ... }

@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
def "I better be quick" { ... }

Si utilizamos la anotación a nivel de clase y a nivel de método, el método siempre sobreescribirá a lo especificado a nivel de clase.

@Timeout(10)
class TimedSpec extends Specification {
  def "I fail after ten seconds"() { ... }
  def "Me too"() { ... }

  @Timeout(value = 250, unit = MILLISECONDS)
  def "I fail much faster"() { ... }
}
ConfineMetaClassChanges

Utilizar metaprogramación puede ser muy beneficioso en nuestros tests pero al mismo tiempo, puede llegar a ser algo peligroso, ya que necesitamos asegurarnos que las clases que han sido metaprogramadas no conservan esa metaprogramación más allá de donde sea imprescindible.

Para ello, bien asegurar de dejar la metaclase sin ninguna modificación con

Class.metaClass = null

o bien, en los casos de los tests con Spock podemos utilizar la anotación @ConfineMetaClassChanges.

@Stepwise
class FooSpec extends Specification {
  @ConfineMetaClassChanges
  def "I run first"() {
    when:
    String.metaClass.someMethod = { delegate }

    then:
    String.metaClass.hasMetaMethod('someMethod')
  }

  def "I run second"() {
    when:
    "Foo".someMethod()

    then:
    thrown(MissingMethodException)
  }
}
Title y Narrative

Podemos especificar lenguaje natural al nombre de nuestras clases con las anotaciones @Title

@Title("This is easy to read")
class ThisIsHarderToReadSpec extends Specification {
  ...
}

y @Narrative.

@Narrative(""""
As a user
I want foo
So that bar
""")
class GiveTheUserFooSpec() { ... }

6.4. Ejercicios

6.4.1. Testeando nuestra propia restricción (0.25 puntos)

Realiza los tests necesarios para comprobar el correcto funcionamiento de la restricción que creaste en sesiones anteriores para que la fecha de recordatorio nunca pueda ser posterior a la fecha de la propia tarea.

Este test o tests los vamos a escribir en la clase TodoSpec.groovy.

6.4.2. Testeando la clase TodoController (0.50 puntos)

Escribe las modificaciones necesarias a la clase TodoControllerSpec para pasar los tests unitarios para la parte de scaffolding. Recuerda que estamos utilizando el servicio todoService en el controlador con lo que tendrás que inyectar el servicio en concreto en el controlador.

Además, realiza los tests unitarios necesarios para comprobar también el funcionamiento de los métodos listNextTodos(Integer days) y showListByCategory() (la vista que nos permite que categorías queremos filtrar en el listado de las tareas).

Estos tests los vamos a escribir en la clase TodoControllerSpec.groovy.

6.4.3. Testeando la clase TodoService (0.50 puntos)

Para terminar con los tests, vamos a implementar un par de tests para testear el servicio TodoService. Estos dos métodos serán countNextTodos() y saveTodo() (que creamos en la sesión anterior).

El método saveTodo() se encarga de almacenar la nueva instancia pero además os recuerdo que debía almacenar la fecha de realización de la tarea si esta había sido realizada.

Estos tests los vamos a escribir en la clase TodoServiceSpec.groovy.

Importante

Es absolutamente necesario que cuando presenteis el proyecto todos los tests se pasen correctamente. Así que antes de entregar el proyecto final, recuerda ejecutar el comando grails test-app unit: para comprobar que tus tests se ejecutan correctamente. Es decir, así debería terminar vuestro proyecto antes de ser entregado.

Todos los tests en verde