Compartir
Publicidad

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

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

No es ninguna sorpresa que en el mundo Java la herramienta de test más utilizada sea JUnit si tenemos en cuenta que fue creada hace más de 15 años. Esto, sin embargo, no implica que sea la mejor, sino que en muchas ocasiones seguimos utilizando las mismas herramientas por inercia o porque aquí siempre se ha hecho esto así sin plantearnos si existen alternativas mejores. Una de estas alternativas es Spock.

Spock es un framework de tests basado en Groovy que podemos utilizar para testear tanto aplicaciones Java como Groovy. Con Spock podemos escribir tests muy expresivos, fáciles de leer y mantener. Todo ello es posible por dos motivos principalmente: el magnífico DSL que proporciona Spock y la potencia de Groovy, lenguaje con el que escribimos los tests.

Proporciona un runner de JUnit por lo que es compatible con cualquier herramienta, IDE y servidor de integración continua que utilicemos actualmente con JUnit. Porque, no nos engañemos, aunque conocemos la importancia de escribir tests, a todos nos da pereza escribirlos en ocasiones, y si encima las herramientas no ayudan, tenemos un motivo más para no hacerlo. En este artículo veremos lo que Spock puede hacer por nuestros tests.

Empezando con Spock

Empezar a escribir tests con Spock es muy sencillo, lo único que tenemos que hacer es añadir a nuestro proyecto Java la dependencia testCompile 'org.spockframework:spock-core:1.0-groovy-2.4' y, opcionalmente, testCompile 'org.codehaus.groovy:groovy-all:2.4.7' si queremos utilizar una versión de Groovy distinta a la 2.4.1 incluída con Spock.

Creando nuestro primer test

Vamos a crear nuestro primer test y explicar cada parte en detalle:

import spock.lang.Specification

class MiPrimerTest extends Specification {             // 1

    void 'invertir una cadena de texto'() {            // 2
        given: 'una cadena de text'                    // 3
            def miCadena = 'Hola Genbetadev'

        when: 'la invertimos'                          // 4
            def cadenaInvertida = miCadena.reverse()

        then: 'se invierte correctamente'              // 5
            cadenaInvertida == 'vedatebneG aloH'
    }
}

Con un primer vistazo seguro que nos llaman la atención varias partes del test:

  1. Todos nuestros tests deben heredar de spock.lang.Specification puesto que en esa clase se encuentra definido el runner compatible con JUnit.
  2. El nombre de cada test se escribe como una cadena de texto entre comillas. Se acabó el nombrar los test como testInvertirCadenaDeTexto o similar. Ahora podemos escribir nombres de tests muy descriptivos y que realmente expresan el motivo del test.
  3. En su forma más general todos los tests de Spock se basan en los bloques given, when, then, siendo given en el que establecemos el estado inicial de nuestro test.
  4. En el bloque when describimos los estímulos, es decir, lo que queremos testear.
  5. Finalmente, en la parte then pondremos las condiciones que se deben cumplir para que el test pase. Es importante notar que lo que escribamos en este bloque son aserciones, por lo que no es necesario usar assert delante de cada una de ellas.

Cabe también destacar que podemos escribir opcionalmente un pequeño texto explicativo en cada una de los bloques, given, when, then. De hecho se considera una buena práctica porque hace que podamos leer todo el test sin tener que mirar en detalle el código del mismo.

Una alternativa a escribir el test anterior que podemos usar en ocasiones para reducir la verbosidad del mismo es utilizar el bloque expect en el que directamente definimos nuestras expectativas.

void 'invertir una cadena de texto'() {
    expect:
        'Hola Genbetadev'.reverse() == 'vedatebneG aloH'
}

¿Y si falla un test?

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

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

Imaginemos el siguiente test. Queremos comprobar que el primer lenguaje de un usuario es Java. Este test falla porque como vemos, tenemos una lista de lenguajes y el primero de ellos es Groovy.

void 'El nombre del primer lenguaje es Groovy'() {
    given: 'información de un usuario'
        def info = [
            nombre  : 'Iván',
            lenguajes: [
                [nombre: 'Groovy', conocimientos: 10], [nombre: 'Java', conocimientos: 9]
            ]
        ]

    expect: 'su primer lenguaje es Java'
        info.lenguajes.nombre.first() == 'Java'
}

Si ejecutamos el test, además de indicarnos que ha fallado, Spock nos mostrará la siguiente salida:

Condition not satisfied:

info.lenguajes.nombre.first() == 'Java'
|    |         |      |       |
|    |         |      Groovy  false
|    |         [Groovy, Java] 5 differences (16% 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 forma que podemos ver claramente todos los valores y saber por qué ha fallado el test sin necesidad de depurar ni de println's adicionales.

Una de las killer features de Spock: Data driven testing

En multitud de ocasiones tenemos que testear el mismo código pero con distintos datos de entrada. Además, en ocasiones, el setup necesario para probar el test no es despreciable por lo que en la práctica acabamos con una gran cantidad de tests en los que el 90% del código es el mismo y sólo cambian los datos y el resultado. Para solucionar este problema podemos usar lo que Spock llama Data driven testing.

void 'comprobando el máximo entre dos números'() {
    expect:
        Math.max(a, b) == resultado

    where:
        a  | b | resultado
        1  | 4 | 4
        5  | 2 | 5
        -1 | 3 | 3
}

Creo que el test es suficientemente explicativo por sí mismo. Hemos creado una tabla de datos y Spock ejecutará el test tres veces sustituyendo en cada una de ellas las variables a, b y resultado con los valores de cada línea. Esta aproximación, aunque directa y muy visual, tiene un pequeño problema. Si cualquiera de las iteraciones hace fallar el test, sólo sabremos que el test ha fallado pero no podremos saber exactamente cual de ellas lo ha hecho fallar. Para solucionarlo, añadiremos la anotación @Unroll y además podremos sustituir el nombre de las variables en el propio nombre del test.

@Unroll
void 'El máximo entre dos números #a y #b es #resultado'() {
    expect:
        Math.max(a, b) == resultado

    where:
        a  | b | resultado
        1  | 4 | 4
        5  | 2 | 5
        -1 | 3 | 3
}

Así, el resultado ahora será:

Spock Data Driven Test

Aprovechando la potencia de Groovy

Hasta ahora hemos visto alguna de las principales características de Spock pero hay otra que puede que hayamos pasado de largo: Groovy. Independientemente de que estemos testeando código Java o Groovy nuestros tests se escriben siempre en Groovy.

Aunque a priori no pueda parecer importante, o incluso pienses que no quieres aprender un lenguaje nuevo para escribir los tests, en poco tiempo entenderás por qué es tan importante Groovy para conseguir esa expresión y legibilidad en nuestros tests. Como creo que la mejor forma de mostrarlo es con un ejemplo. Imaginemos que tenemos el siguiente método que queremos testear que simplemente devuelve una lista de personas.

public static List makePersonList() {
    return Arrays.asList(
        new Person("Sheldon", "Cooper"),
        new Person("Leonard", "Hofstadter"),
        new Person("Raj", "Koothrappali"),
        new Person("Howard", "Wolowitz")
    );
}

Siendo Person el siguiente POJO:

public class Person {

    private String name;
    private String lastName;

    public Person() {
    }

    public Person(String name, String lastName) {
        this.name = name;
        this.lastName = lastName;
    }

    // Getters y setters omitidos
}

Ahora podemos escribir el siguiente test:

void 'testeando una lista de personas'() {
    when:
        def personList = DataHelper.makePersonList()

    then:
        personList.size() == 4
        personList.name == ['Sheldon', 'Leonard', 'Raj', 'Howard']        // 1
        personList.name.sort() == ['Howard', 'Leonard', 'Raj', 'Sheldon']
        personList.lastName.collect { it.size() } == [6, 10, 12, 8]
        personList.name.min { it.length() } == 'Raj'
}

¿Vemos algo raro en personList.name? ¿Qué significa esto? ¿De verdad tenemos un List<Person> y estamos accediendo a name en esa lista? En realidad lo que está ocurriendo es que estamos aprovechándonos del syntactic sugar que añade Groovy para poder extraer el nombre de cada una de las personas que están en la lista. El código anterior lo podríamos escribir también de las siguientes formas, siendo cada una de ellas un poco más parecida a Java, hasta llegar a la última que sería el equivalente en Java 8:

personList.name
personList*.name // spread operator
personList.collect { it.name }
personList.collect { Person p -> p.name }
personList.collect { Person p -> p.getName() }
personList.stream().map { it.name }.collect(Collectors.toList())
personList.stream().map { p -> p.getName() }.collect(Collectors.toList())

Como veis, por el simple hecho de utilizar Groovy para nuestros test hemos conseguido éstos sean muy expresivos y fáciles de entender.

Testeando código con colaboradores

En nuestro código del día a día tenemos clases y objetos que interaccionan entre ellos, se llaman entre sí y son dependencias unos de otros. El problema viene cuando queremos testear uno de estos métodos que tiene algún colaborador y no queremos que éste interfiera en nuestro test. En el mundo Java estamos acostumbrados a utilizar frameworks de mocking como JMock, EasyMock o Mockito, que aunque pueden ser utilizados con Spock, no son necesarios pues Spock incluye el suyo propio sin necesidad de añadir una dependencia adicional.

Finalmente, según el comportamiento que queramos obtener de nuestros colaboradores, usaremos Mock o Stub.

Mocks

Un mock es un objeto sin comportamiento en el que podremos llamar a un método pero no habrá ningún efecto adicional más allá de devolver el valor por defecto del propio método. La idea es utilizarlo para comprobar que un método se ha ejecutado e incluso con qué parametros lo ha hecho.

Un ejemplo en el que se puede ver la utilidad de un mock es el siguiente: Imaginemos que estamos dando de alta un usuario en nuestra plataforma (método createUser) y la acción final es notificar por email (notificationService.sendNotification) a ese usuario de que su cuenta ha sido creada correctamente. Durante la ejecución de los tests de este método no queremos enviar un correo electrónico pero queremos asegurarnos de que el método se llama correctamente y queremos que el test falle si no es así.

public class UserService {

    private NotificationService notificationService;

    public UserService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    void createUser(String nombre, String apellido) {
        Person person = new Person(nombre, apellido);

        // Comprobaciones
        // Lógica de negocio
        // Almacenar el usuario en base de datos

        notificationService.sendNotification(person, "Usuario creado correctamente");
    }
}

El test sería así:

void 'notificacion enviada cuando se crea un usuario'() {
    given:
        def mockedNotificationService = Mock(NotificationService)
        def userService = new UserService(mockedNotificationService)

    when:
        userService.createUser('Iván', 'López')

    then:
        1 * mockedNotificationService.sendNotification(_, 'Usuario creado correctamente')
}

Lo primero que hacemos es crear el mock de NotificationService y lo inyectamos al crear el servicio que estamos testeando UserService. Después, en el bloque then podemos comprobar que el método de envío de notificaciones has sido ejecutado solamente una vez en nuestro mock. Si el método no se ejecuta o lo hace más de una vez, el test fallará.

Stubs

Si en lugar de simplemente querer asegurarnos de que un método se ha ejecutado necesitamos que éste devuelva un determinado valor o lance una excepción, lo que haremos será un stub.

Un caso de uso para un stub puede ser que necesitemos obtener algún valor de la base de datos para continuar con nuestro test. Ejemplo:

public interface PersonRepository {
    Person findById(Long id);
}

Y el test sería:

void 'obtenemos un valor predeterminado'() {
    given:
        def stubbedRepository = Stub(PersonRepository) {
            findById(_) >> new Person('Iván', 'López')
        }

    when:
        def person = stubbedRepository.findById(1)

    then:
        person.name == 'Iván'
        person.lastName == 'López'

}

El anterior es un ejemplo muy básico pero un stub en Spock se puede personalizar de diferentes formas. Según nuestras necesidades podemos querer devolver distintos valores en llamadas consecutivas al mismo método, lanzar una excepción o incluso devolver un valor en función de los parámetros de entrada.

Resumen

Hemos visto una introducción muy rápida y básica a Spock que espero sirva para que os planteeis introducirlo en algún proyecto y empezar a utilizarlo poco a poco. Nos dejamos muchas cosas en el tintero: Comprobación de excepciones, extensiones (@IgnoreRest, @IgnoreIf, @Stepwise,...) el método old, comparadores de Hamcrest... Si teneis curiosidad por conocer todos estos detalles os remito a la documentación oficial. Para profundizar aún más os recomiendo el libro Java Testing with Spock que fue publicado en diciembre de 2015 e incluye los últimos cambios hasta Spock 1.0.

Todos los ejemplos incluidos en este artículo se encuentran en un repo de Github para que podais probar y ejecutar todo vosotros mismos.

Temas
Publicidad
Comentarios cerrados
Publicidad
Publicidad
Inicio