Spock Framework Basics

Estos días estoy preparando un “workshop” sobre Spock, de modo que quería aprovechar para contaros que es Spock y que uso hacemos de él en el día a día en nuestros desarrollos.

Spock es un “framework” para testing de aplicaciones Java y Groovy del cual destacaría:

  • Utiliza Groovy como lenguaje, lo cual permite tener tests altamente expresivos.
  • Utiliza Sputnik, como JUnit runner, integrándose perfectamente con la mayoría de IDEs actuales y herramientas de Building.
  • Los tests se definen haciendo uso de una estructura de bloques siguiendo el estilo BDD (behavior-driven development), permitiéndonos integrar fácilmente nuestras historias de usuario y sirviéndonos a su vez como documentación de la especificación.
  • Cuenta con su propia API para la creación de Mocks, haciendo muy simple la creación de test basados en interacciones.
  • Hace uso de las “Power assertions” de Groovy, mostrando información muy valiosa en caso de que nuestras assertions fallen.
  • Cuenta con una consola web para poder probar nuestras especificaciones.

Comencemos con un ejemplo sencillo:

Lo primero que necesitaremos será importar el package spock.lang que contiene los tipos mas importantes para escribir nuestra especificación.

import spock.lang.*

La especificación consiste en una clase Groovy que extiende de spock.lang.Specification:

class MyFirstSpecification extends Specification {  
  // fields
  // fixture methods
  // feature methods
  // helper methods
}

Fields

Utilizaremos “instance fields” para almacenar objetos que pertenecen al fixture de la especificación, sería semánticamente equivalente a instanciar los objetos en el método setup(). Es importante tener en cuenta que los objetos almacenados en “instance fields” no son compartidos entre “feature methods” , cada método recibirá su propio objeto. De modo que si lo que nos interesara es compartir un objeto entre “feature methods” por que el coste de creación del objeto es alto o por que nos pueda interesar por la naturaleza del test que los diferentes métodos puedan interactuar con el mismo objeto, definiremos un @Shared field lo cual es equivalente a definir el objeto en el método setupSpec().

@Shared res = new VeryExpensiveResource()

Fixture Methods

def setup() {}          // run before every feature method  
def cleanup() {}        // run after every feature method  
def setupSpec() {}      // run before the first feature method  
def cleanupSpec() {}    // run after the last feature method  

Los “fixture methods” se encargarán de inicializar o limpiar el entorno en el que los “feature methods” son ejecutados.

setup() y cleanup() se ejecutan dentro del contexto de los “feature methods” son semánticamente equivalentes a los métodos anotados con @Before and @After en JUnit, mientras que los* setupSpec()* y cleanupSpec() se ejecutan en el contexto de la especificación siendo equivalentes a métodos anotados con @BeforeClass y @AfterClass en JUnit.

Feature Methods

Los “feature methods”, conocidos como “test methods” en JUnit son el punto central dentro de la especificación y se utilizan para describir el comportamiento del SUT (System Under Test).

Una de las principales características es la definición del nombre del método usando “Strings”.

class UserSpec extends Specification {  
  // … 
  def “Add 3 and 4 to get the correct result”() { 
    // … 
  } 
}

Setup Blocks

Se utilizan para inicializar los objetos que usaremos en los “feature methods”, no puede ir precedido de ningún otro bloque, se trata de un bloque opcional , pero en caso de no definirlo se creará un bloque implícito y “given” representa un alias para acercarnos más a la semántica del BDD.

When and Then Blocks

Los bloques “When” contienen la acción que queremos testear, conocida como el estímulo. Siempre suceden junto con un bloque “then” donde comprobaremos si la respuesta satisface ciertas condiciones.

En un “feature method” podrían crearse más de un par “when-then”.

En un bloque “then” solo podremos comprobar condiciones, excepciones, interacciones o definir variables.

Condiciones:

La condiciones describen el estado esperado, a diferencia de la assertions den JUnit, usamos expresiones boolean. De modo que Spock esperará siempre como resultado de una condición true o false.

user.getName() == “Alvaro”  
user.getLastName() == "Salazar"  
!user.isEnabled()

Todas las expresiones dentro de un bloque “then” o “expect” serán tratadas como condiciones , excepto las expresiones clasificadas como interacciones (que comentaremos mas adelante) o “void methods”.

Otra de las utilidades de las condiciones es la posibilidad de comprobar que un acción lanza un excepción tras aplicarle el estímulo. Para ello usaremos el método “thrown()” , que recibe como parámetro el tipo de excepción que esperamos.

when:  
user.getUser()

then:  
thrown(UserNotFoundException)  

Puede ocurrir que tengamos un caso en el que lo que nos interese sea comprobar precisamente que tras el estímulo aplicado al SUT no se lanza ningún tipo de excepción. En este caso haremos uso del método “notThrown()”.

Como comentábamos antes, dentro de un bloque a parte de las condiciones podemos definir interacciones, la cuales describirán como se comunican unos objetos con otros, es decir centraremos nuestro foco en el comportamiento de los objetos en lugar de en su estado.

Las interacciones están directamente relacionadas con el uso del MockingApi de Spock que comentaremos en posteriores post, en los cuales hablaremos en más detalle sobre la creación de Mocks y la verificación de su comportamiento.

Expect Blocks

Se trata de bloques con igual comportamiento que el caso de los “then”, pero limitados a la definición de condiciones o variables. Su principal uso será cuando sea más natural definir el estímulo y el valor esperado en una sola expresión.

expect:  
Math.max(1, 2) == 2  

Cleanup Blocks

Semánticamente tiene el mismo funcionamiento que los métodos “cleanup()” usándose para liberar recursos usados en los “feature methods”. Un detalle a tener en cuenta es que el bloque se ejecutará incluso si en la ejecución del “feature method” se produce un excepción.

Where Blocks

Son siempre los últimos bloques en el método y nunca se pueden repetir en el método. Se usan para escribir métodos “data-driven”, lo cuales veremos en siguientes post en más detalle.

Con esto acabamos el primer post sobre Spock en el que nos hemos centrado en describir las funcionalidades básicas con las que cuenta el framework y que hacen que sea mi primera opción a la hora e testear aplicaciones java o Groovy.

En siguientes post entraremos a ver en más detalle el uso de Spock, así como el uso de su API para la creación de Mock, Stub, Spies…, la variación que ofrece sobre JUnit para la creación de “parameterized tests” denominados en Spock “data driven test”, en fin todos aquellas características que hacen de Spock una opción muy a tener en cuenta en nuestros tests.

Bibliografía:

  1. http://spockframework.github.io/spock/docs/1.0/spock_primer.html
  2. https://touk.pl/blog/2013/09/07/spock-basics/
  3. https://github.com/spockframework/spock-example
  4. https://dzone.com/articles/introduction-spock