Unit Testing – Como mockear un método de una clase padre

Recientemente, en el proyecto en el que estoy, nos hemos encontrado con un problema curioso a la hora de hacer los tests unitarios de una funcionalidad. El método que queríamos testear llamaba a un método de la clase padre, el cual hacia ciertas llamadas a base de datos y diversos recursos externos. No había manera de inicializar esos recursos como mocks antes, por la implementación de la clase padre, y no sabíamos como podíamos mockear solo esa parte, dado que no es otra clase, es la clase padre la que intentamos mockear, lo cual inicialmente no es posible…

Solución (la «regular» pero la más práctica)

Podemos mockear un método de una clase que estemos probando (que el método sea de la clase padre o no, no cambia nada, si es de la clase padre es como si fuera de la actual, lo hereda), usando los Spy de Mockito.

Veamos un poco por encima la diferencia entre Mock y Spy (en el framework Mockito):

  • Mock: cuando Mockito crea un Mock, lo único que hace es crear un esqueleto de la clase, sin funcionalidad alguna. Al llamar a los métodos del mock, no hará nada (aparte del comportamiento que le asignemos con los métodos when, thenReturn, etc)
  • Spy: Al crear un spy, es necesario pasarle un objeto YA creado de esa clase. Y mockito lo que hace realmente es una especie de wrapper sobre ese objeto. Al llamar a los métodos del spy, se ejecutarán los métodos de la clase (a no ser que hayamos cambiado el comportamiento, como vamos a hacer ahora).

Ejemplo:

Pongamos que tenemos estas dos clases. La primera, que tiene un método que usa algún recurso como base de datos o similar, y la segunda que hereda de esta y usa ese método dentro de uno propio:

public class Foo {

    public String doSomethingWithDatabase() {
        System.out.println("Method that connect with DB...");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Finished connection whit DB.");

        return "OK real Foo";
    }
}

public class Bar extends Foo {

    public int myMethod() {
        System.out.println("Method to test!");
        String msg = this.doSomethingWithDatabase();
        System.out.println("Msg: ".concat(msg));

        return 1;
    }
}

¿Como testeamos nuestra clase ‘Bar’, mockeando el método de la clase padre? Así:

package com.edwise.pocs.spymockito;

// imports...

public class BarTest {

    private Bar bar;

    @Test
    public void testMyMethodWithMockitoSpy() {
        bar = spy(new Bar());
        doReturn("OK Foo mocked").when(bar).doSomethingWithDatabase();

        int result = bar.myMethod();

        assertThat(result).isEqualTo(1);
    }
}

Creamos un Spy con el método estático de mockito pasándole una instancia ya creada de nuestra clase ‘Bar’. Y ahora, sobre esa instancia hacemos tanto el test (linea 14), como el cambio de comportamiento típico de mockito (linea 12).
Para mockear el método usamos ‘doReturn‘ y no ‘when‘, ya que con when no funcionaría… Con los Spy es necesario usar doReturn (ver javadoc de doReturn para más detalle)

Solución (la correcta…)

Realizar un test como el anterior realmente no es lo más correcto, estamos medio mockeando la clase que estamos probando… Ojo, funciona, y hace lo que tiene que hacer. Pero no es algo muy limpio, mezclamos en la misma instancia la clase testeada y la clase mockeada.
Si necesitamos hacer algo como esto para testear nuestra clase, lo que está pasando es que nuestro diseño no es correcto. En este caso, como en otros muchos, lo que ocurre es que estamos abusando de la herencia, y seguramente la relación entre nuestras dos clases debería ser composición (‘Bar’ no extendería ‘Foo’ y tendría un atributo de tipo ‘Foo’, probablemente pasado en el constructor, o por inyección de dependencias), algo como esto:

public class BarWellDesigned {

    private Foo foo;

    public BarWellDesigned(Foo foo) {
        this.foo = foo;
    }

    public int myMethod() {
        System.out.println("Method to test!");
        String msg = foo.doSomethingWithDatabase();
        System.out.println("Msg: ".concat(msg));

        return 1;
    }
}

La clase Foo sería idéntica a la anterior.

De esta manera nuestro test sería el típico en el que mockeamos la clase de la que dependemos (‘Foo’ en este caso) y testeariamos ‘Bar’ directamente sin ‘wrappearlo’ con el Spy de Mockito.

package com.edwise.pocs.spymockito;

// imports...

public class BarWellDesignedTest {

    private BarWellDesigned barWellDesigned;

    @Test
    public void testMyMethodWithoutMockito() {
        Foo foo = mock(Foo.class);
        barWellDesigned = new BarWellDesigned(foo);
        when(foo.doSomethingWithDatabase()).thenReturn("OK Foo mocked");

        int result = barWellDesigned.myMethod();

        assertThat(result).isEqualTo(1);
    }
}

Pero claro, en el mundo real, cuando te encuentras algo así, hacer un cambio de diseño (o pedir que lo haga el equipo que mantiene ese código), suele ser complicado…

Podéis descargaros todo el código de este post en mi github, proyecto spy-mockito-example.

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).

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.