Tests de integración usando REST-assured

Vamos a volver un poco más con el testing 🙂 En mi último post sobre este tema, tests de integración contra un servicio REST, con Spring, expliqué como implementar esos tests de integración con las herramientas que nos provee Spring (MockMvc principalmente). Como ya comenté en alguno de mis posts de testing, hay otras librerías para realizar este tipo de test, como es REST-Assured. Y es la que vamos a usar en este para implementar unos tests completos contra un servicio REST, de manera muy sencilla y legible.

Concepto:
En varios posts anteriores expliqué el concepto de tests de integración y configuramos tanto maven como Spring para poder implementarlos, y en otro implementé los tests usando MockMvc de Spring y la librería jsonpath.
En este caso, voy a implementar unos tests de integración contra un servicio REST (el mismo usado en los últimos posts, un REST para un recurso ‘Info’), pero esta vez lo haré usando la librería REST-Assured, en lugar del clásico MockMvc. Está librería nos ofrece una manera muy limpia y eficiente de realizar este tipo de tests, en «formato» BDD: Given – When – Then
Como base usaré el mismo proyecto que ya monté en esos posts, con toda la configuración de maven y Spring para tener tests de integración.

Entorno usado:
Java JDK 1.8
Maven 3.2.5
Git 1.9.5
IDE Intellij 14.1.3 Ultimate version

Pasos:

1. Recordamos como es el servicio REST a testear, este es el controller:

package com.edwise.pocs.itrestassured.controller;

// imports...

@RestController
@RequestMapping("/api/info/")
public class InfoController {

    @Autowired
    private InfoService infoService;

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(method = RequestMethod.GET,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public List<Info> getAllInfos() {
        return infoService.findAll();
    }

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(method = RequestMethod.GET, value = "{id}",
            produces = MediaType.APPLICATION_JSON_VALUE)
    public Info getInfo(@PathVariable Long id) {
        return infoService.findOne(id);
    }

    @RequestMapping(method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Info> createInfo(@RequestBody Info info) {
        Info infoCreated = infoService.save(info);
        return new ResponseEntity<>(createHeadersWithLocation(infoCreated),
                HttpStatus.CREATED);
    }

    @ResponseStatus(HttpStatus.NO_CONTENT)
    @RequestMapping(method = RequestMethod.PUT, value = "{id}")
    public void updateInfo(@PathVariable Long id, @RequestBody Info info) {
        infoService.update(info.setId(id));
    }

    @ResponseStatus(HttpStatus.NO_CONTENT)
    @RequestMapping(method = RequestMethod.DELETE, value = "{id}")
    public void deleteInfo(@PathVariable Long id) {
        infoService.delete(id);
    }

    private HttpHeaders createHeadersWithLocation(Info info) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setLocation(
                ServletUriComponentsBuilder
                        .fromCurrentRequest()
                        .path("/{id}")
                        .buildAndExpand(info.getId())
                        .toUri());
        return httpHeaders;
    }
}

Es el mismo controller del post anterior de testing, excepto que he implementado el método del post como debe ser.

2. Añadimos la dependencia de la librería REST-assured en nuestro pom.xml:

        <dependency>
            <groupId>com.jayway.restassured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>2.4.1</version>
            <scope>test</scope>
        </dependency>

Con ‘scope’ test, por supuesto, solo lo necesitamos en nuestros tests.

3. Tendremos la misma clase de tests de integración que en el post anterior:

package com.edwise.pocs.itrestassured.controller;

// imports...

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebIntegrationTest({"server.port=0"})
public class InfoControllerIT {
    
}

Con la misma configuración de Spring, ya explicada en su momento.

4. Primero necesitamos configurar un pequeño detalle en REST-assured: el puerto al que atacar. En nuestro test de integración, con la anotación ‘@WebIntegrationTest({«server.port=0»})’ lo que le decimos a Spring, entre otras cosas, es que arranque un servidor con todo el contexto necesario, en el puerto que le decimos ahí. Si el puerto es 0, usará un puerto aleatorio… así que haremos lo siguiente para configurarlo en REST-assured:

package com.edwise.pocs.itrestassured.controller;

// imports...

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebIntegrationTest({"server.port=0"})
public class InfoControllerIT {

    @Value("${local.server.port}")
    private int serverPort;

    @Before
    public void setUp() {
        RestAssured.port = serverPort;
    }
   
}

Con la anotación de Spring @Value obtenemos el puerto sobre el que se ha arrancado, y en el método @Before de Junit se lo seteamos a Rest-Assured.

5. Implementamos nuestro primer test con REST-assured, el método GET /api/info/{id}, que obtiene un objeto Info por su id. Lo implementaremos así:

    @Test
    public void getInfo_InfoFound_ShouldReturnCorrectInfo() {
        given()
            .accept(ContentType.JSON)
            .pathParam("id", INFO_ID_1234)
        .when()
            .get("/api/info/{id}")
        .then()
            .statusCode(HttpStatus.SC_OK)
            .body(INFO_TEXT_FIELD, equalTo(INFO_TEXT_1234))
            .body(CREATION_DATE_TIME_FIELD, notNullValue());
    }

Todo el test es una linea de código (aunque lo hemos separado por partes, para ser más legible). En la parte de ‘given‘ prepararíamos ciertas cosas del test, en la de ‘when‘ realizamos el test, y en ‘then‘ hacemos las comprobaciones o ‘assertions’. El método ‘given’ lo importamos estaticamente.
En este caso, marcamos como que esperamos un json, seteamos en el parámetro id de la url un id, y con eso hacemos la petición GET. Después comprobamos el código http, y con el método ‘body‘ podemos hacer comprobaciones sobre los campos del json devuelto, en este caso con matchers de hamcrest (equalTo y notNullValue).

6. Implementamos ahora de la misma manera el GET /api/info/ que nos devolverá todos:

    @Test
    public void getAll_InfosFound_ShouldReturnFoundInfos() {
        given()
            .accept(ContentType.JSON)
        .when()
            .get("/api/info/")
        .then()
            .statusCode(HttpStatus.SC_OK)
            .body(INFO_TEXT_FIELD,
                    hasItems(INFO_TEXT_1234, INFO_TEXT_4567, INFO_TEXT_7892))
            .body(CREATION_DATE_TIME_FIELD, everyItem(notNullValue()));

    }

Muy similar al anterior, aquí en las comprobaciones con el método ‘body’ usamos otros matchers de hamcrest, en este caso que afectan a listas (hasItems y everyItems). Comprobando siempre cómodamente sobre un campo del json.

7. Continuamos con los POST y PUT, respectivamente:

    @Test
    public void postInfo_InfoCorrect_ShouldReturnCreatedStatusAndNoBody() {
        given()
            .body(createMockInfo(INFO_TEXT_1234_NEW, CREATION_DATE_TIME_NEW))
            .contentType(ContentType.JSON)
        .when()
            .post("/api/info/")
        .then()
            .statusCode(HttpStatus.SC_CREATED)
            .body(isEmptyString())
            .header(HEADER_LOCATION, containsString("/api/info/" + INFO_ID_1234));
    }

    @Test
    public void putInfo_InfoCorrect_ShouldReturnNoContentStatus() {
        given()
            .body(createMockInfo(INFO_TEXT_1234_UPDATED, CREATION_DATE_TIME_UPDATED))
            .contentType(ContentType.JSON)
            .pathParam("id", INFO_ID_1234)
        .when()
            .put("/api/info/{id}")
        .then()
            .statusCode(HttpStatus.SC_NO_CONTENT);
    }

    private Info createMockInfo(String text, LocalDateTime creationDateTime) {
        return new Info()
                .setInfoText(text)
                .setCreationDateTime(creationDateTime);
    }

En los dos casos, en el ‘given’, con ‘body’ lo que hacemos es pasarle el objeto a enviar. Podriamos pasarselo en formato json en un String, pero no es necesario, él se encarga de hacer el parseo. En el caso del POST, en las comprobaciones, revisamos que el ‘location’ de la respuesta corresponda con la url del nuevo recurso creado.

8. Y por último, el DELETE:

    @Test
    public void deleteInfo_ShouldReturnNoContentStatus() {
        given()
            .pathParam("id", INFO_ID_1234)
        .when()
            .delete("/api/info/{id}")
        .then()
            .statusCode(HttpStatus.SC_NO_CONTENT);
    }

Nada nuevo en este caso.

Con esto habriamos terminado nuestro test de integración. La clase completa la podeís ver aquí.
Como vemos, nos quedan unos tests de integración muy limpios, y siguiendo la «nomenclatura» típica de Given-When-Then, y en plan ‘fluent interface’. A mi personalmente me gustan bastante los tests usando MockMvc, pero hay que reconocer que REST-assured facilita mucho tanto la implementación como la legibilidad de nuestros tests.

Si queréis descargaros el proyecto completo, lo tenéis como siempre en mi github, proyecto integrationtests-restassured-example.

 

Tests de integración para un servicio REST, con Spring

En mis dos últimos post he explicado como configurar los tests de integración con Spring Boot y como configurarlos además con Maven. Teniendo ya toda esa configuración, es hora ya de implementar unos tests de integración completos, para un servicio REST.

Concepto:
Ya expliqué en los anteriores post el concepto de tests de integración: en resumen, son tests en los que probamos varios componentes de un sistema trabajando juntos (a diferencia de los unitarios, en los que solo probamos cada componente por separado).
En este ejemplo voy a realizar unos tests de integración completos de un servicio REST (el mismo usado en los últimos posts, un servicio REST para un recurso sencillo ‘Info’). Para realizar los tests correctamente usaré, por un lado el clásico MockMvc de Spring, y para probar los jsons que devuelve el rest usaré la librería jsonpath.

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.5
IDE Intellij 14.0.3 Ultimate version

Pasos:

1. Los tests van a tratar simplemente de probar todo el servicio REST Info el cual tenemos implementado en InfoController:

package com.edwise.pocs.integrationtestsrest.controller;

// imports...

@RestController
@RequestMapping("/api/info/")
public class InfoController {

    @Autowired
    private InfoService infoService;

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(method = RequestMethod.GET,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public List<Info> getAllInfos() {
        return infoService.findAll();
    }

    @ResponseStatus(HttpStatus.OK)
    @RequestMapping(method = RequestMethod.GET, value = "{id}",
            produces = MediaType.APPLICATION_JSON_VALUE)
    public Info getInfo(@PathVariable Long id) {
        return infoService.findOne(id);
    }

    @RequestMapping(method = RequestMethod.POST,
            produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Info> createInfo(@RequestBody Info info) {
        Info infoCreated = infoService.save(info);
        return new ResponseEntity<>(infoCreated, HttpStatus.CREATED);
    }

    @ResponseStatus(HttpStatus.NO_CONTENT)
    @RequestMapping(method = RequestMethod.PUT, value = "{id}")
    public void updateInfo(@PathVariable Long id, @RequestBody Info info) {
        infoService.update(info.setId(id));
    }

    @ResponseStatus(HttpStatus.NO_CONTENT)
    @RequestMapping(method = RequestMethod.DELETE, value = "{id}")
    public void deleteInfo(@PathVariable Long id) {
        infoService.delete(id);
    }

}

Como se puede ver, he completado el controller de los posts anteriores con un método PUT (para actualizar) y un método GET all, para tener lo que podría ser un servicio REST completo.

2. Añadimos a las dependencias de maven la librería jsonpath, para testear fácilmente jsons:

     ...
     <dependencies>
          ...
         <dependency>
             <groupId>com.jayway.jsonpath</groupId>
             <artifactId>json-path-assert</artifactId>
             <version>1.2.0</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
     ...

2. Comenzamos nuestros tests de integración con la configuración que hicimos en los posts anteriores, tanto de maven como de Spring y Spring Boot. Para ello usaremos la clase que ya creamos, InfoControllerIT:

package com.edwise.pocs.integrationtestsrest.controller;

// imports...

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebIntegrationTest({"server.port=0"})
public class InfoControllerIT {

    @Autowired
    protected WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(this.webApplicationContext)
                .build();
    }

    // TODO aquí implementaremos los tests
}

3. Creamos nuestro primer test. En el vamos a probar el método GET /api/info/{id}, es decir, el que obtiene un objeto Info por su id. Lo implementaremos así:

    @Test
    public void getInfo_InfoFound_ShouldReturnCorrectInfo() throws Exception {
        mockMvc.perform(get("/api/info/{id}", INFO_ID_1234))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id", is(INFO_ID_1234.intValue())))
                .andExpect(jsonPath("$.infoText", is(INFO_TEXT_1234)))
                .andExpect(jsonPath("$.creationDateTime", is(notNullValue())));
    }

Todos nuestros tests de integración consisten en una llamada con el objeto MockMvc. En ella se ve como realizamos un get, con un parámetro de path variable (id), y sobre esa llamada hacemos varías comprobaciones (métodos ‘andExpect‘):
– Que el http status devuelto es OK (200).
– Que el contenido recibido es de tipo JSON.
– Usando la librería jsonpath comprobamos que el body de la respuesta contiene un json correcto (en este caso compruebo que el id y el texto son los correctos y que la fecha no es nula, por ejemplo).

4. Implementamos el test que pruebe el método que devuelve todos los Info’s, GET /api/info/:

    @Test
    public void getAll_InfosFound_ShouldReturnFoundInfos() throws Exception {
        mockMvc.perform(get("/api/info/"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$", hasSize(3)))
                .andExpect(jsonPath("$[0].id", is(INFO_ID_120.intValue())))
                .andExpect(jsonPath("$[0].infoText", is(INFO_TEXT_1234)))
                .andExpect(jsonPath("$[0].creationDateTime", is(notNullValue())))
                .andExpect(jsonPath("$[1].id", is(INFO_ID_121.intValue())))
                .andExpect(jsonPath("$[1].infoText", is(INFO_TEXT_4567)))
                .andExpect(jsonPath("$[1].creationDateTime", is(notNullValue())))
                .andExpect(jsonPath("$[2].id", is(INFO_ID_122.intValue())))
                .andExpect(jsonPath("$[2].infoText", is(INFO_TEXT_7892)))
                .andExpect(jsonPath("$[2].creationDateTime", is(notNullValue())));
    }

El tests es muy similar al anterior, lo único que en este caso nos devuelve un array de Info’s, con lo que comprobamos el tamaño del array (método hasSize), y cada uno de los Info’s por separado, con jsonpath, accediendo a cada Info como si fuera un array.

5. Seguimos con el test que pruebe el método crea un nuevo Info, POST /api/info/, y el que actualice un Info ya existente, PUT /api/info/{id}:

    @Test
    public void postInfo_InfoCorrect_ShouldReturnCreatedStatusAndCorrectInfo() throws Exception {
        mockMvc.perform(post("/api/info/")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"infoText\":\"Info 1234 New\",\"creationDateTime\":\"2013-10-11T20:10:10\"}"))
                .andExpect(status().isCreated())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id", is(INFO_ID_1234.intValue())))
                .andExpect(jsonPath("$.infoText", is(INFO_TEXT_1234_NEW)))
                .andExpect(jsonPath("$.creationDateTime", is(notNullValue())));
    }

    @Test
    public void putInfo_InfoCorrect_ShouldReturnNoContentStatus() throws Exception {
        mockMvc.perform(put("/api/info/{id}", INFO_ID_1234)
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"infoText\":\"Info 1234 Updated\",\"creationDateTime\":\"2014-10-25T19:16:21\"}"))
                .andExpect(status().isNoContent());
    }

En el primero, realizamos un POST y le marcamos como ‘Content-Type’ del mismo el típo ‘application/json’. Luego le pasamos como contenido de la request el json del objeto a crear. Las comprobaciones son las de siempre, comprobando en este caso que el http status es CREATED (201).
En el segundo, realizamos un PUT, con path variable (el id de siempre) y hacemos lo mismo que en el POST, excepto que no comprobamos el json del body (no devuelve nada) y que el http status esperado es NO CONTENT (204).

6. Por último, implementamos el que pruebe el método que borre un Info, DELETE /api/info/{id}:

    @Test
    public void deleteInfo_ShouldReturnNoContentStatus() throws Exception {
        mockMvc.perform(delete("/api/info/{id}", INFO_ID_1234))
                .andExpect(status().isNoContent());
    }

Realizamos un DELETE, con el id path variable, y solo es necesario comprobar el http status, NO CONTENT (204).

7. Y hemos terminado. Nuestra clase completa ‘InfoControllerIT’ quedaría así. Podemos ahora ejecutar los tests tanto desde Intellij como con maven (profile ‘integration-test’, con el goal ‘verify’).

8. Temas importantes a tener en cuenta:
– El servicio REST implementado es muy sencillo y tiene la capa de datos mockeada. Si tuviéramos un servicio completo, con su capa de base de datos, habría que buscar como configurar una base de datos en memoria o similar, para realizar los tests (o incluso mockear con mockito la capa de datos).
– Faltarían varios tests, principalmente tests que fallen: GET, PUT y DELETE sobre un Info que no exista, etc.
– El servicio REST, al realizar un POST, devuelve en el body de la response el json completo. Realmente, lo más correcto sería no devolver nada en el body, y devolver en el ‘location’ de los headers un link al nuevo Info creado (principio HATEOAS). Esto probablemente lo veamos en otro post más adelante.

Si queréis descargaros el proyecto completo, lo tenéis como siempre en mi github (proyecto integrationtests-rest-example).

Cómo configurar tus tests de integración con Maven

En mi anterior post expliqué como configurar los tests de integración con Spring / Spring Boot. Un tema que deje pendiente es como configurar esos tests para ejecutarse aparte de los tests unitarios, y para mantenerlos separados. Y ese es el tema que trataré en este, en concreto, con Maven.

Concepto:
Como ya comente en aquel post, los tests de integración no están pensados para ejecutarse a menudo (a diferencia de los tests unitarios), y lo lógico es ejecutarlos solo en algunos casos en la maquina en la que desarrollamos y sobre todo en nuestro sistema de integración continua.

Por ello lo recomendable respecto a estos tests lo siguiente:

  • Tener los tests de integración en otra paquetería o subcarpeta distinta de los tests unitarios: yo personalmente prefiero en una subcarpeta distinta, pero también los he visto en algún proyecto en la misma subcarpeta (‘test/java’) que los unitarios, pero con alguna paquetería en plan ‘integrationtest.resto.paqueteria’. Para este post lo haré como a mi me gusta más, en una subcarpeta distinta: ‘integration-test/java’. Todo esto se configura fácilmente con Maven.
  • Configurar los tests de integración para ser ejecutados en casos distintos a los unitarios: esto también lo haremos con Maven. Principalmente lo haré creando perfiles distintos en maven, para ejecutar los tests de integración solo cuando estemos con el perfil correspondiente, así como con distintos ‘goals’ de maven.

El proyecto sobre el que voy a mostrar como configurar los tests de integración con Maven es exactamente el mismo que vimos en el post anterior: un sencillo servicio REST sobre un recurso ‘Info’, implementado con capas Controller, Service y Repository (esta última mockeada).

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.5
IDE Intellij 14.0.3 Ultimate version

Pasos:

1. Primero vamos a mover nuestra única clase de test de integración (InfoControllerIT) a su propio subdirectorio de tests de integración. Creamos una subcarpeta bajo ‘src’ que llamaremos ‘integration-test’ y bajo ella otra que llamaremos ‘java’. Para poder crear ahora una paquetería java a partir de esa carpeta, desde Intellij, en File -> Project Structure, seleccionamos la opción ‘Modules’ y ahí podemos marcar como carpeta de tipo ‘Tests’:

Project Structure window

Project Structure window, Intellij

2. Ahora podemos crear la paquetería sobre la nueva carpeta ‘java’. (al ser un test de integración yo le pongo la misma paquetería que si fuera un test del controller, que además es la misma que tenía antes, pero podéis ponerle la que querais). Ahora ya sí, movemos nuestra clase InfoControllerIT a su nuevo directorio y paquetería. Solo con esto ya podemos ejecutar los tests unitarios y de integración separadamente desde Intellij (botón derecho en la subcarpeta ‘java’ de los que queramos lanzar, y ‘Run All Tests’).

3. Pero lo que nos interesa realmente es separarlos a nivel de las ejecuciones maven, así que vamos a configurarlo. Primero, creamos dos perfiles, uno para desarrollo, ‘dev’, (solo tests de unitarios) y otro para tests de integración, ‘integration-test’.

...
    <profiles>
        <profile>
            <id>dev</id>
        </profile>
        <profile>
            <id>integration-test</id>
            <properties>
                <build.profile.id>integration-test</build.profile.id>
                <skip.integration.tests>false</skip.integration.tests>
                <skip.unit.tests>true</skip.unit.tests>
            </properties>
        </profile>
    </profiles>

    <properties>
        ...
        <build.profile.id>dev</build.profile.id>
        <skip.integration.tests>true</skip.integration.tests>
        <skip.unit.tests>false</skip.unit.tests>
    </properties>
...

Con los tag profiles y profile definimos cada perfil de maven. Además, por cada perfil podemos definir unas propiedades. En nuestro caso necesitamos esas propiedades «por defecto» (para ‘dev’), en nuestras properties generales (lineas 19, 20 y 21), y las mismas properties definidas en el perfil ‘integration-test’. Los nombres de las propiedades son bastante autoexplicativos, son booleanos que usaremos para evitar ejecutar unos tests u otros según el perfil.

4. Vamos a añadir el primer plugin de maven que necesitamos: Build Helper Maven Plugin. Sirve para añadir más funcionalidades a los ‘goals’ de maven. En nuestro caso lo vamos a usar para añadir ‘src/integration-test/java’ como otra subcarpeta de tests (por defecto maven solo cuenta como tests los de ‘src/test/java’):

...
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>build-helper-maven-plugin</artifactId>
            <version>1.9.1</version>
            <executions>
                <execution>
                    <id>add-integration-test-sources</id>
                    <phase>generate-test-sources</phase>
                    <goals>
                        <goal>add-test-source</goal>
                    </goals>
                    <configuration>
                        <sources>
                            <source>src/integration-test/java</source>
                        </sources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
    ...
</build>
...

En ‘phase’ le decimos en que fase de maven se debe ejecutar, y en ‘goals’ el goal del build helper plugin que vamos a ejecutar (tiene varios más, en nuestro caso nos interesa el de añadir otra ‘fuente’ de tests, add-test-source). En la configuración del plugin ponemos la nueva carpeta de tests (src/integration-test/java).

5. Añadimos el plugin Maven Surefire Plugin. Este plugin, que de por si ya viene por defecto aunque no lo definamos en el pom, es el que se encarga de la ejecución de los tests unitarios y de generar informes con los resultados. Normalmente no es necesario añadirlo y basta con la configuración por defecto, pero en este caso necesitamos un par de cosas:

...
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.18.1</version>
            <configuration>
                <skipTests>${skip.unit.tests}</skipTests>
                <excludes>
                    <exclude>**/*IT.java</exclude>
                </excludes>
            </configuration>
        </plugin>
        ...
    </plugins>
    ...
</build>
...

Le añadimos en el tag ‘skipTests’ el boolean para evitar los tests unitarios (si somos perfil ‘dev’ se ejecutarán, si somos perfil ‘integration-test’, no.
También le añadimos ciertas clases a excluir, en este caso los .java que terminen en ‘IT’ (asi es como se suelen nombrar los tests de integración). Realmente este plugin ya de por si solo pilla las clases que terminen en ‘Test’, ‘TestCase’ o empiecen por ‘Test’, con lo que esto no es estrictamente necesario, pero yo prefiero tenerlo por si acaso seguimos otro tipo de nomenclatura en los de integración.

6. Por último, añadimos el plugin Maven Failsafe Plugin. Este plugin es el encargado de la ejecución de tests de integración, generando el war/jar aunque los tests fallen. Este no viene por defecto con maven, a diferencia del anterior:

...
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>2.18.1</version>
            <executions>
                <execution>
                    <id>integration-tests</id>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                    <configuration>
                        <skipTests>${skip.integration.tests}</skipTests>
                    </configuration>
                </execution>
            </executions>
        </plugin>

        ...
    </plugins>
    ...
</build>
...

Con las tags ‘goals’/’goal’ le decimos en que idem de maven ejecutar los tests de integración, y en configuración le ponemos en ‘skipTests’ el booleano para evitar la ejecución de los mismos (si somos perfil ‘dev’ no se ejecutaran, si somos perfil ‘integration-test’, sí)

7. ¡A probar! Ya lo tenemos preparado todo, para probar basta con que desde la tool window de maven de intellij elijamos el perfil (Profiles), ‘dev’ o ‘integration-test’, y ejecutar el goal que queramos. Para ser más concretos, los comandos por consola serían:

  • Para ejecutar los tests unitarios:
    mvn test
  • Para generar el jar (ejecutando tests unitarios):
    mvn package
  • Para ejecutar solo los de integracion, cualquiera de estos dos:
    mvn verify -P integration-test
    mvn integration-test -P integration-test

No es necesario poner que estamos en perfil ‘dev’ en las dos primeras porque es el perfil por defecto, pero podriamos hacerlo, claro.

De esta manera, mientras desarrollemos en local estaremos siempre en perfil ‘dev’, con lo que nunca se ejecutaran los tests de integración. Si en cualquier momento queremos ejecutar los de integración con maven, nos pasaremos a perfil ‘integration-test’.

Tenéis el proyecto completo y ya configurado como siempre en mi github, proyecto integrationtests-maven-example.

Spring Boot series: cómo configurar tus tests de integración

Vuelvo con Spring Boot, esta vez con un pequeño post en el que voy a explicar como configurar fácilmente los tests de integración con él. Y así empiezo a tocar por fin un poco el testing, que ya me apetecía escribir un post sobre ello 🙂

Concepto:
Mientras que los tests unitarios son tests en los que probamos cada unidad / clase / pequeña funcionalidad por separado, en los tests de integración se prueba todo (o casi todo) el sistema (o grupo de componentes) interactuando entre ellos.

También, a diferencia de los tests unitarios, que son muy sencillos de configurar (normalmente no necesitan configuración), los tests de integración suelen ser algo liosos y complejos de configurar. Con Spring, y en concreto con Spring Boot, veremos que es bastante sencillo.

Por otro lado, los tests de integración no están pensados para ejecutarse a menudo (a diferencia de los unitarios), y lo normal es ejecutarlo solo en algunos casos en nuestra maquina local y sobre todo en nuestro sistema de integración continua. Por eso lo normal es tener estos tests en otra carpeta distinta a la típica de los tests unitarios y además tener configurada la ejecución de esos tests en otro ‘goal’ (si usamos maven).
Esto último lo dejaré para explicarlo en otro post. En este me voy a centrar sólo en la parte de configuración de Spring. En este post la clase de test de integración la voy a crear en el mismo directorio ‘test’ de siempre.

El proyecto sobre el que voy a mostrar como configurar los tests de integración es un sencillo servicio REST sobre un recurso ‘Info’. Está implementado con las típicas capas Service y Repository (esta última mockeada).
También dejo para otro post el realizar unos tests de integración completos de un servicio REST.

Entorno usado:
Java JDK 1.8
Maven 3.2.1
Git 1.9.5
IDE Intellij 14.0.3 Ultimate version

Pasos:

1. Creamos el proyecto base sobre el que implementaremos nuestros tests:

  • Creamos un proyecto maven en Intellij, como siempre: File -> New Project, elegimos Maven, checkeamos «Create from archetype» y seleccionamos el quickstart básico o el mio si lo tenéis instalado.
  • Añadimos en nuestro pom.xml las dependencias básicas de Spring Boot: el parent, el starter de web, y el plugin para maven (más info en el post sobre como montar un PoC de Spring Boot, pasos 5 y 6.
  • Creamos como siempre nuestra clase base para Spring Boot, Application.java.
  • Implementamos nuestro REST. Con las siguientes clases: la entity Info, interfaz InfoRepository, implementación ‘mock’ de ese ‘Repository’, interfaz InfoService, e implementación de ese ‘Service’.
  • Por último, implementamos un pequeño controller para terminar nuestro REST. Es bastante básico, solo he implementado los métodos GET, POST y DELETE:
    package com.edwise.springbootseries.integrationtests.controller;
    
    // imports
    
    @RestController
    @RequestMapping("/api/info/")
    public class InfoController {
    
        @Autowired
        private InfoService infoService;
    
        @ResponseStatus(HttpStatus.OK)
        @RequestMapping(method = RequestMethod.GET, value = "{id}",
                produces = MediaType.APPLICATION_JSON_VALUE)
        public Info getInfo(@PathVariable Long id) {
            return infoService.findOne(id);
        }
    
        @RequestMapping(method = RequestMethod.POST,
                produces = MediaType.APPLICATION_JSON_VALUE)
        public ResponseEntity createInfo(@RequestBody Info info) {
            Info infoCreated = infoService.save(info);
            return new ResponseEntity<>(infoCreated, HttpStatus.CREATED);
        }
    
        @ResponseStatus(HttpStatus.NO_CONTENT)
        @RequestMapping(method = RequestMethod.DELETE, value = "{id}")
        public void deleteInfo(@PathVariable Long id) {
            infoService.delete(id);
        }
    }
    

    Como puede verse, usa el ‘InfoService’ definido anteriormente.

2. Vamos ya al lío con nuestro test. Primero, añadimos las dependencias necesarias para testing: el ‘starter’ de tests para spring boot (spring-boot-starter-test). Entre otras librerías, este starter nos incluye junit, mockito, hamcrest, spring-test… Nos quedaría un pom.xml tal que así:

...
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.1.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.5.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
...

Como veis, también he añadido también la dependencia de la librería de jackson para parsear la nueva Java date API (jsr310), jackson-datatype-jsr310. La necesitamos ya que nuestro entity tiene un campo fecha de ese tipo. (Revisad mi post anterior sobre Spring Boot y jackson para más información. Importante: para que el formato del campo fecha se parsee correctamente es necesario añadir una propiedad al application.properties, tanto en el directorio ‘main’ como en el ‘test’).

3. Crearemos una clase con sufijo ‘IT’ (es lo que se suele poner a los tests de integración para diferenciarlos de los unitarios) en el subdirectorio ‘test’ y le pondremos varias anotaciones Spring:

package com.edwise.springbootseries.integrationtests;

import org.junit.runner.RunWith;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebIntegrationTest({"server.port=0"})
public class InfoControllerIT {
    // tests here
}

Con estas tres anotaciones tendremos un test de integración completamente funcional:
@RunWith(SpringJUnit4ClassRunner.class): runner básico de Spring que necesitamos para cualquier test en el que necesitemos un contexto de Spring.
@SpringApplicationConfiguration: anotación alternativa a la clásica @ContextConfiguration, pero para Spring Boot. Con ella le pasamos información de como configurar el contexto. Lo normal es ponerle la clase básica de Spring Boot (nuestra Application.java). También podríamos añadirle otras clases de configuración especificas para los tests.
@WebIntegrationTest: con esta, le decimos que necesitamos probar una ‘web application’ (nos permitirá crear un mockMvc, entre otras cosas), y además nos levanta un servidor para test completo. Le podemos pasar el puerto en el que arrancar, si le ponemos 0, como es nuestro caso, será aleatorio. Está anotación es similar a poner estas dos: @WebAppConfiguration y @IntegrationTest

4. Para probar una aplicación web o, en este caso, un servicio REST, necesitamos poder lanzar peticiones http. Para simular eso, en Spring, lo normal es usar la clásica clase MockMvc. Spring Boot además provee una nueva clase para testing: TestRestTemplate. Y hay otras librerías, como REST-Assured En este caso, crearemos el test con la MockMvc, en próximos posts entraré más en detalle con el resto.
Primero es necesario construir un MockMvc, para ello añadiremos lo siguiente a nuestra ‘InfoControllerIT’:

...
    @Autowired
    protected WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void setUp() {
        mockMvc = MockMvcBuilders
                .webAppContextSetup(this.webApplicationContext)
                .build();
    }
...

Creamos en el método @Before el mockMvc con el builder que nos provee Spring. Es necesario pasarle como parámetro el objeto con la información de contexto, WebApplicationContext, el cual lo podemos obtener gracias a la anotación @WebIntegrationTest (o @WebAppConfiguration).

5. Con eso ya tendríamos configurado completamente nuestro test de integración. Añadimos ahora 3 tests básicos, para cada uno de los métodos del servicio REST (GET, POST y DELETE):

...

    @Test
    public void getInfo_ShouldReturnCorrectInfo() throws Exception {
        String jsonExpected = "{\"id\":1234,\"info\":\"Info 1234\",\"creationDateTime\":\"2001-12-12T13:40:30\"}";

        mockMvc.perform(get("/api/info/{id}", INFO_ID_1234))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().string(jsonExpected));
    }

    @Test
    public void postInfo_ShouldReturnCreatedStatusAndCorrectInfo() throws Exception {
        String jsonExpected = "{\"id\":1234,\"info\":\"Info 1234 New\",\"creationDateTime\":\"2015-10-25T19:13:21\"}";

        mockMvc.perform(post("/api/info/")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"info\":\"Info 1234 New\",\"creationDateTime\":\"2015-10-25T19:13:21\"}"))
                .andExpect(status().isCreated())
                .andExpect(content().string(jsonExpected));
        ;
    }

    @Test
    public void deleteInfo_ShouldReturnNoContentStatus() throws Exception {
        mockMvc.perform(delete("/api/info/{id}", INFO_ID_1234))
                .andExpect(status().isNoContent());
    }
...

En próximos posts entraré más en detalle, pero el código es bastante autoexplicativo: como se puede ver, en cada test, llamamos al método ‘perform’ de nuestro mockMvc, pasándole si es get, post…, y sobre ese mismo objeto, hacemos varios ‘asserts’ (andExpected es el método concreto) sobre el status devuelvo por la respuesta, contenido, etc.

Nuestra clase de test quedaría finalmente así.

Así de fácil. Si queréis jugar con el código, podéis bajaroslo como siempre de mi github, proyecto springbootseries-integrationtests.

En próximos posts explicaré como separar los tests de integración de los tests unitarios, con maven, e implementaré algún ejemplo más en detalle de como probar un servicio REST a fondo.