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.