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.

 

Implementar un cliente REST con Spring: RestTemplate

He hablado ya mucho de servicios REST y de como montarlos pero, y ¿si queremos acceder a un servicio REST desde nuestra aplicación? Hay varias maneras, desde hacerlo «a pelo» creando una conexión http, o usando clientes Rest. Para este post vamos a ver en concreto el cliente de Spring, RestTemplate.

Concepto:
Spring nos provee de un cliente Rest, RestTemplate, muy sencillo de usar. El se encarga de realizar la conexión http, lo único que hace falta es pasarle la url del servicio contra el que conectar. El mismo RestTemplate gestiona sus propios ‘messageConverters’, con los que parsear los datos enviados y recibidos de/a json, por ejemplo. Solo es necesario añadir en nuestro proyecto la dependencia necesaria (Jackson, en nuestro caso).

En el ejemplo veremos como llamar al servicio con los métodos básicos (GET, POST, PUT, DELETE). Para hacerlo más sencillo, lo haremos directamente desde un programa java desde su método ‘main’. Además, lo haremos atacando contra un servicio definido en apiary. También realizaremos otro ejemplo contra un servicio arrancado en local, en concreto el proyecto integrationtests-rest-example de mi github.

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

Ejemplo con apiary:

1. Voy a usar como servicio uno definido en mi cuenta de apiary, con un recurso ‘book’ Para el que no conozca apiary, es una web que nos ofrece el poder definir ‘mocks’ de servicios REST, contra los que poder atacar. Es muy útil a la hora de desarrollar.
En concreto vamos a atacar contra este servicio: http://docs.booksapi.apiary.io/

2. Nos creamos un proyecto maven básico (por ejemplo, con el archetype ‘maven-archetype-quickstart’), y añadimos las siguientes dependencias:

   
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>4.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.3</version>
        </dependency>
        ...
    </dependencies>

La dependencia de ‘spring-web’ es precisamente para poder tener acceso a la clase RestTemplate. La ‘jackson-databind’ es necesaria para el parseo de datos a json.

3. Creamos una clase con método main, en la que implementaremos los ejemplos:

package com.edwise.pocs.springrestclient;

public class AppBookRestApiary {

    private static final String URL_API_BOOKS =
            "http://private-114e-booksapi.apiary-mock.com/books/";

    public static void main(String[] args) {

    }

}

Tendremos la url a nuestro servicio (http://private-114e-booksapi.apiary-mock.com/books/) directamente en una constante.

4. Creamos una clase para la entity que nos devuelve el servicio:

package com.edwise.pocs.springrestclient.model;

public class Book {
    private Long id;
    private String title;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", title='" + title + '\'' +
                '}';
    }
}

Necesitamos este bean o entity para recuperar los datos, etc. Es tan sencillo como revisar el servicio, ver sus campos e implementar una clase java con esos campos. Jackson se encargará del parseo a/desde json.

5. Creamos nuestro objeto RestTemplate, con el que accederemos al servicio. Al crearse, por defecto ya tiene los converters necesarios para el parseo a/desde json, al tener como dependencia la librería Jackson.

    public static void main(String[] args) {
        RestTemplate restTemplate = new RestTemplate();

    }

Es asi de simple. Tiene también disponible algún otro constructor, con parámetros, como los converters, etc. Pero con el constructor sin parámetros nos sobra.

6. Ahora probaremos cada uno de los métodos que acepta el servicio:

– GET all: para obtener todos los ‘books’:

    ResponseEntity<Book[]> response =
                restTemplate.getForEntity(URL_API_BOOKS, Book[].class);

    System.out.println();
    System.out.println("GET All StatusCode = " + response.getStatusCode());
    System.out.println("GET All Headers = " + response.getHeaders());
    System.out.println("GET Body (object list): ");
    Arrays.asList(response.getBody())
                .forEach(book -> System.out.println("--> " + book));

Tanto para get como para post, tenemos disponible dos tipos de métodos: getForObject/postForObject y getForEntity/postForEntity. Los segundos te devuelven una respuesta completa (es el que uso en el ejemplo), en la que poder obtener tanto headers, como código http devuelto, etc. Los ‘xxxForObject’ devuelven directamente el objeto (el body de la respuesta).
En este caso, le tenemos que pasar la url y el tipo esperado para el body (array de clase ‘Book’ en nuestro caso). Mostramos luego el código http, los headers y, en bucle, todos los ‘books’ devueltos. El método ‘getBody’ nos devolverá un ‘Book[]’, al haberlo parametrizado así en la llamada al get.

– GET: para obtener un ‘book’ en concreto, por su id:

    ResponseEntity<Book> response =
                restTemplate.getForEntity(URL_API_BOOKS + "{id}", Book.class, 12L);

    System.out.println();
    System.out.println("GET StatusCode = " + response.getStatusCode());
    System.out.println("GET Headers = " + response.getHeaders());
    System.out.println("GET Body (object): " + response.getBody());

Similar al caso anterior. En este caso, le pasamos Book.class como tipo esperado.
Por otro lado, el tercer parámetro serían las ‘pathvariables’ del endpoint. En este caso el id.

– POST: para insertar un ‘book’ nuevo:

    Book bookToInsert = createBook(null, "New book title");
    ResponseEntity<Book> response =
                restTemplate.postForEntity(URL_API_BOOKS, bookToInsert, Book.class);

    System.out.println();
    System.out.println("POST executed");
    System.out.println("POST StatusCode = " + response.getStatusCode());
    System.out.println("POST Header location = " + response.getHeaders().getLocation());

Creamos un nuevo ‘Book’, sin id, y lo enviamos por post, con un ‘postForEntity’. En el segundo parámetro le pasamos lo que seria el ‘body’ de la request (nuestro objeto) y en el tercer parámetro el tipo.
En este caso no mostramos el body de la respuesta, dado que lo único que nos devuelve es el ‘location’ (más info en mi anterior post)

– PUT: para actualizar un ‘book’ en concreto, por su id:

    Book bookToUpdate = createBook(123L, "Book title updated");
    restTemplate.put(URL_API_BOOKS + "{id}", bookToUpdate, 123L);

    System.out.println();
    System.out.println("PUT executed");

Aquí cambia un poco la cosa. Excepto para get y post, para el resto de métodos http, RestTemplate nos ofrece unos métodos más simples, demasiado en mi opinión. Como vemos en este caso, el método ‘put’ no devuelve nada. Si necesitáis poder comprobar el código http, los headers o algún otro dato de la respuesta, echadle un vistazo al método ‘exchange’. Es un método con el que podemos realizar cualquier tipo de llamada (GET, POST, etc).

– DELETE: para borrar un ‘book’ en concreto, por su id:

    restTemplate.delete(URL_API_BOOKS + "{id}", 12L);

    System.out.println();
    System.out.println("DELETE executed");

Igual que el caso de put 😦

Ejemplo contra un servicio en local:

1. Si no lo tenemos ya, nos descargamos mi proyecto integrationtests-rest-example de mi github, y lo arrancamos con ‘mvn spring-boot:run’.

2. Como antes, creamos una clase con método main, en la que implementaremos los ejemplos (igual que antes) y también creamos una clase para la entity que nos devuelve el servicio:

package com.edwise.pocs.springrestclient.model;

import java.time.LocalDateTime;

public class Info {

    private Long id;
    private String infoText;
    private LocalDateTime creationDateTime;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getInfoText() {
        return infoText;
    }

    public void setInfoText(String infoText) {
        this.infoText = infoText;
    }

    public LocalDateTime getCreationDateTime() {
        return creationDateTime;
    }

    public void setCreationDateTime(LocalDateTime creationDateTime) {
        this.creationDateTime = creationDateTime;
    }

    @Override
    public String toString() {
        return "Info{" +
                "id=" + id +
                ", infoText='" + infoText + '\'' +
                ", creationDateTime=" + creationDateTime +
                '}';
    }
}

En este caso usamos la nueva API Date de Java 8. Para que Jackson la parsee correctamente, tenemos que añadir una dependencia más:

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

4. El resto es prácticamente igual al caso anterior, no voy a poner el código, pero está disponible en mi github: AppInfoRestLocal

Tenéis todo el código de los dos ejemplos preparado para probarlo en mi github, proyecto spring-rest-client-example. Se puede ejecutar cada ejemplo ejecutando su clase ‘main’, la cual ejecuta todos los casos.