Prueba tus API REST con Rest Assured

3
21029

En este tutorial vamos a ver cómo probar de manera muy sencilla el funcionamiento de cualquier API REST que se nos ponga por delante

Prueba tus API REST con Rest Assured

Indice de contenidos

  1. Introducción
  2. Entorno
  3. Nuestra primera prueba con Rest Assured
  4. Guardando el cuerpo de la respuesta
  5. Mostrar el contenido de request y response
  6. Conclusiones
  7. Referencias

1. Introducción

De un tiempo a esta parte, es cada vez más común que las aplicaciones expongan su funcionalidad mediante API REST, que pueda ser consumida
por aplicaciones JavaScript, aplicaciones móviles, etc.

Y, al igual que usamos herramientas como Concordion y Selenium para asegurar que la funcionalidad que ofrece nuestra aplicación web es correcta,
lo mismo debemos hacer con nuestros API. La ventaja es que para probar estas piezas disponemos desde hace tiempo de un buen conjunto de herramientas
que nos permiten comprobar la funcionalidad que exponemos (HttpClient para hacer las invocaciones, Jackson para manipular JSON, etc.). Pero son
utilidades pensadas para realizar peticiones y no expresamente para probar.

Hoy vamos a conocer Rest Assured una herramienta pensada específicamente para probar llamadas a API REST y,
en general, cualquier pertición que se ejecute sobre el protocolo HTTP.

2. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Portatil MacBook Pro 17′ (2.8GHz Intel Core i7, 8GB DDR3)
  • Sistema operativo: MacOS Sierra 10.12.3
  • Entorno de desarrollo: IntelliJ IDEA 2017.1
  • JDK 1.8
  • Rest Assured 3.0.2

3. Nuestra primera prueba con Rest Assured

Para hacer nuestras pruebas, he decidido utilizar un API REST ya existente, concretamente «swapi», que maneja información del universo de Star Wars.
Podéis encontrar este API y su documentación en swapi.co

A continuación os pego el contenido del test, y posteriormente comentamos los aspectos más curiosos

  package com.autentia.tutoriales;

  import io.restassured.RestAssured;
  import org.junit.Test;

  import static org.hamcrest.Matchers.*;

  public class SWApiTestWithRestAssured {

      @Test
      public void whenRequestingAResourceThenLinksToResourcesMustBeReturned() {

          String body = RestAssured
                  .given()
                      .baseUri("http://swapi.co/api")
                      .and()
                      .queryParam("format", "json")
                  .when()
                      .get("/")
                  .then()
                      .log().all()
                      .and().assertThat().statusCode(is(equalTo(200)))
                      .and()
                      .body("films", response -> notNullValue())
                      .body("vehicles", response -> notNullValue())
                      .body("people", response -> notNullValue())
                      .body("starships", response -> notNullValue())
                      .body("species", response -> notNullValue())
                      .body("planets", response -> notNullValue())
                      .and().extract().body().asString();
      }

  }

Lo primero que observamos es que la forma de describir la prueba con Rest Assured sigue el formato Gherkin (Given – When – Then), casi un estándar de la
definición de escenarios de prueba.

Además, vemos también que podemos configurar para las peticiones tanto la URI a la que atacarán como los parámetros que se van
a enviar en el «query string». También podríamos, si quisiéramos, añadirle un cuerpo, subir un fichero con una petición «multipart»… Lo que se os ocurra.
También podemos configurar el nivel de log que queremos para la response (volcar toda la petición, sólo las cabeceras, o el cuerpo, o…), y lo mismo podríamos
hacer con la request. Esta es la salida que se mostraría en este caso, en que hemos solicitado que se vuelque la información de la respuesta

  
  HTTP/1.1 200 OK
  Date: Thu, 30 Mar 2017 07:49:29 GMT
  Content-Type: application/json
  Transfer-Encoding: chunked
  Connection: keep-alive
  Set-Cookie: __cfduid=d4fc0a55b94153e4e73f5bb8d7137491e1490860169; expires=Fri, 30-Mar-18 07:49:29 GMT; path=/; domain=.swapi.co; HttpOnly
  X-Frame-Options: SAMEORIGIN
  Allow: GET, HEAD, OPTIONS
  Vary: Accept, Cookie
  Etag: W/"1f7a4766c9ebf66cdb1ddb85d5cc6f2f"
  Via: 1.1 vegur
  Server: cloudflare-nginx
  CF-RAY: 347978f890a70ded-MAD
  Content-Encoding: gzip

  {
      "people": "http://swapi.co/api/people/",
      "planets": "http://swapi.co/api/planets/",
      "films": "http://swapi.co/api/films/",
      "species": "http://swapi.co/api/species/",
      "vehicles": "http://swapi.co/api/vehicles/",
      "starships": "http://swapi.co/api/starships/"
  }

En este caso, se trata de una petición usando el verbo GET pero, evidentemente, el framework soporta peticiones usando prácticamente todos los verbos
HTTP (soporta GET, POST, PUT, PATCH, DELETE, HEAD y OPTIONS). Y además, una vez realizada la petición, el mismo framework nos posibilita realizar
aserciones sobre el contenido de la respuesta, usando los «matchers» de Hamcrest, que aportan bastante legibilidad a las pruebas,
encargándose él de parsear la información, lo que nos facilita bastante la vida.

También es posible extraer el cuerpo de la respuesta si necesitamos hacer validaciones más complejas
o si queremos guardarlo para usar algún elemento en futuras peticiones. En este ejemplo lo hemos almacenado en un String,
pero podemos usar el objeto que queramos, incluso uno creado por nosotros (En el peor de los casos, será necesario crear un mapper específico

para nuestro objeto, pero seguirá siendo posible)

Para demostrar lo dicho en los párrafos anteriores, y más cosas interesantes de Rest Assured, vamos a seguir jugando con el framework…

4. Guardando el cuerpo de la respuesta

En el siguiente ejemplo, vamos a modificar el test anterior para guardar el cuerpo de la respuesta en un objeto propio, y posteriormente usarlo para invocar a otro endpoint

    package com.autentia.tutoriales;

    import io.restassured.RestAssured;
    import org.junit.Test;

    import static org.hamcrest.Matchers.*;

    public class SWApiTestWithRestAssured {

        @Test
        public void whenRequestingAResourceThenLinksToResourcesMustBeReturned() {

            BaseApiResponse baseApiResponse = RestAssured
                .given()
                    .baseUri("http://swapi.co/api")
                    .and()
                    .queryParam("format", "json")
                    .log().all()
                .when()
                    .get("/")
                .then()
                    .statusCode(is(equalTo(200)))
                    .and()
                    .body("films", response -> notNullValue())
                    .body("vehicles", response -> notNullValue())
                    .body("people", response -> notNullValue())
                    .body("starships", response -> notNullValue())
                    .body("species", response -> notNullValue())
                    .body("planets", response -> notNullValue())
                    .and().extract().body().as(BaseApiResponse.class);

            RestAssured
                .given()
                    .queryParam("format", "json")
                    .log().all()
                .when()
                    .post(baseApiResponse.getFilms())
                .then()
                    .log().all()
                    .and()
                    .assertThat()
                        .statusCode(is(equalTo(405)));
        }

        private static class BaseApiResponse {
            private String films;
            private String vehicles;
            private String people;
            private String starships;
            private String species;
            private String planets;

            public String getFilms() {
                return films;
            }

            public String getVehicles() {
                return vehicles;
            }

            public String getPeople() {
                return people;
            }

            public String getStarships() {
                return starships;
            }

            public String getSpecies() {
                return species;
            }

            public String getPlanets() {
                return planets;
            }
        }
    }

Al ejecutar el test salta un error… RestAssured indica que no tiene ningún parser de JSON en el classpath,
así que hay que añadir a mi pom.xml la dependencia de Jackson Databind

  
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
      <version>3.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.8.7</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

Si ahora ejecutamos de nuevo, todo va como la seda 🙂

5. Mostrar el contenido de request y response

Hemos visto también que es posible volcar al log el contenido de la request y la response. En las pruebas que hemos visto hasta ahora,
lo hemos hecho añadiendo las llamadas al método «log()», que nos permite configurar el nivel de detalle (de request y response por separado).
Puedo indicar que se trace sólo el cuerpo («log().body()»), el cuerpo y las cabeceras («log().body().and().log().headers()»).

Pero existen otras formas de activar el vocado al log. Imaginemos que no nos interesa tener el detalle cuando todo va bien,
pero que sí queremos que nos muestre la información de request/response en caso de que las validaciones solicitadas fallen. Para ver
cómo hacer esto, vamos a simplificar un poco nuestro test

  
  package com.autentia.tutoriales;

  import io.restassured.RestAssured;
  import org.junit.Test;

  import static io.restassured.config.LogConfig.logConfig;
  import static io.restassured.config.RestAssuredConfig.config;
  import static org.hamcrest.Matchers.*;

  public class SWApiTestWithRestAssured {

      @Test
      public void whenRequestingAResourceThenLinksToResourcesMustBeReturned() {

          RestAssured
              .given()
                  .queryParam("format", "json")
                  .config(config().logConfig(logConfig()
                          .enableLoggingOfRequestAndResponseIfValidationFails()))
              .when()
                  .get("http://swapi.co/api/")
              .then()
                  .assertThat().statusCode(is(equalTo(200)));
      }
  }

Podemos ver que, en la parte del «given» se ha añadido un nuevo elemento, «config()», en le que hemos indicado que queremos habilitar las
trazas sólo cuando fallen las validaciones. De esta manera, si se ejecuta el test tal cual está no mostrará traza, pero si se cambia el «statusCode»
de 200 (success) a 404 (not found) y volvemos a ejecutar sí veremos el detalle completo de la request y la response.

Pero ¿qué pasa si en mi test hago varias llamadas y quiero el mismo comportamiento para cada una de ellas? ¿Tengo que configurar el log en cada petición
que cree? La respuesta es «no»… RestAssured permite habilitar cierta configuración de manera global en lugar de hacerlo en la propia petición. Por ejemplo:

  package com.autentia.tutoriales;

  import io.restassured.RestAssured;
  import org.junit.Test;

  import static org.hamcrest.Matchers.equalTo;
  import static org.hamcrest.Matchers.is;

  public class SWApiTestWithRestAssured {

      @Test
      public void whenRequestingAResourceThenLinksToResourcesMustBeReturned() {

          RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();

          RestAssured
              .given()
                  .queryParam("format", "json")
              .when()
                  .get("http://swapi.co/api/")
              .then()
                  .assertThat().statusCode(is(equalTo(200)));

          RestAssured
              .given()
                  .queryParam("format", "json")
              .when()
                  .post("http://swapi.co/api/films/")
              .then()
                  .assertThat().statusCode(is(equalTo(200)));
      }


  }

Si ejecutamos este test, no sacará traza para la primera llamada, pues se ejecuta correctamente, pero sí para la segunda, puesto que la aserción del statusCode fallará.

Sin embargo, no todo es bonito en el mundo del logging de Rest Assured… Por defecto, las trazas las vuelca directamente a la consola y no hay forma de decirle
directamente que las vuelque en un sistema de traza estilo Log4J o similares. Sin embargo, existen formas de burlar esta limitación… Lo que sí permite el framework
es indicar un PrintStream al que volcar las trazas de la prueba, y los PrintStream reciben como parámetro un OutputStream. Así que lo que vamos a hacer es implementarnos
nuestro propio OutputStream, y decirle que imprima lo que reciba en el logger que le indiquemos:

  package com.autentia.tutoriales;

  import org.slf4j.Logger;

  import java.io.IOException;
  import java.io.OutputStream;

  public class LoggerOutputStream extends OutputStream {

      StringBuilder unflushedContent = new StringBuilder();
      private final Logger logger;

      public LoggerOutputStream(Logger logger) {
          this.logger = logger;
      }

      @Override
      public void write(int b) throws IOException {
          this.unflushedContent.append((char)b);
      }

      @Override
      public void write(byte[] b) throws IOException {
          this.unflushedContent.append(new String(b));
      }

      @Override
      public void write(byte[] b, int off, int len) throws IOException {
          this.unflushedContent.append((new String(b)).substring(off, off + len));
      }

      @Override
      public void flush() throws IOException {
          this.logger.info(unflushedContent.toString());
          this.unflushedContent.setLength(0);
      }

      @Override
      public void close() throws IOException {
          this.flush();
      }
  }

Y ahora modificamos el código de la prueba para que emplee nuestro OutputStream

  package com.autentia.tutoriales;

  import io.restassured.RestAssured;
  import org.junit.Test;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;

  import java.io.IOException;
  import java.io.PrintStream;

  import static io.restassured.config.LogConfig.logConfig;
  import static io.restassured.config.RestAssuredConfig.config;
  import static org.hamcrest.Matchers.equalTo;
  import static org.hamcrest.Matchers.is;

  public class SWApiTestWithRestAssured {

      private final Logger logger = LoggerFactory.getLogger(SWApiTestWithRestAssured.class);

      @Test
      public void whenRequestingAResourceThenLinksToResourcesMustBeReturned() throws IOException {

          try (LoggerOutputStream outputStream = new LoggerOutputStream(logger)) {
              RestAssured.config = config().logConfig(logConfig()
                  .defaultStream(new PrintStream(outputStream))
                  .enableLoggingOfRequestAndResponseIfValidationFails());

              RestAssured
                      .given()
                          .log().method().and().log().uri().and().log().params()
                          .queryParam("format", "json")
                      .when()
                          .get("http://swapi.co/api/")
                      .then()
                          .assertThat().statusCode(is(equalTo(200)));

              outputStream.flush();

              RestAssured
                      .given()
                          .queryParam("format", "json")
                      .when()
                          .post("http://swapi.co/api/films/")
                      .then()
                          .assertThat().statusCode(is(equalTo(200)));

              outputStream.flush();
          }

      }

  }

Con esto ya tendríamos la prueba mandando las trazas a nuestro logger. Aunque el constructor del objeto PrintStream nos permite indicarle
que debe hacer «flush» automáticamente, en este caso es mejor no hacerlo y encargarnos nosotros de hacerlo a mano. Si activáramos el
«autoflush» la traza de nuestra request (o response) quedaría muy «desligada», y perderíamos legibilidad en la misma

6. Conclusiones

Cuando desarrollamos un nuevo software, las pruebas son una parte imprescindible del mismo. Son nuestra red de seguridad para asegurar que las
cosas funcionan como deben y al igual que para el código de producción, es importante conocer y usar frameworks que nos permitan desarrollar
las pruebas de una manera rápida y sencilla (y sin necesidad de reinventar la rueda). Además, puesto que las pruebas nos dicen qué hace nuestra
aplicación, es importante también usar herramientas que ayuden a hacer el código de las mismas lo más legible posible

Rest Assured es una herramienta que nos proporciona todas estas características. Implementar una llamada es un proceso rápido y sencillo, igual que validar
la respuesta obtenida. Además, el API que ofrece permite una verbosidad que hace que el código de la prueba se pueda leer casi como si fuera lenguaje natural,
facilitando mucho la comprensión de lo que se está probando, lo que lo convierte en un framework a tener muy en cuenta.

7. Referencias

3 COMENTARIOS

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad