Descubre cómo automatizar Service Tests con Rest-assured

,

Actualmente en un proceso de verificación o pruebas es cada vez más común incluir pruebas funcionales sobre el backend de la aplicación, que generalmente está bajo un API Rest, con el objetivo de:

  • Detectar fallos más rápidamente antes de pasar a la capa de interfaz de usuario.
  • Diseñar y ejecutar pruebas muy fáciles de automatizar, mantener y muy rápidas de ejecutar.

Este concepto se ve reforzado mediante la conocida pirámide del testing o pirámide de Cohn (Mike Cohn unos de los contribuidores de Scrum). El objetivo es jerarquizar las pruebas por tres tipos (hay muchas variantes), que van desde las pruebas Unitarias o ‘Unit Test’, las pruebas de Servicio/API/Backend o ‘Service Test’ a, finalmente, las pruebas de Interfaz de usuario.

A medida que avanzas desde abajo hacía arriba una prueba clasificada en ese nivel va perdiendo velocidad de ejecución y ganando integración ( a más alto más lento y más integrado), pero también es más difícil de mantener. Además, el volumen de la pirámide se refiere también al número de pruebas (a más bajo, más pruebas).

Si deseas leer más información al respecto, puedes ver aquí un artículo de Ham Vocke sobre la pirámide del testing.

A continuación vamos a explicar cómo realizar un plan de pruebas sobre un Servicio Web Rest y cómo programarlo usando Java, rest-assured y JUnit.

Conceptos previos

  • API: es un conjunto de funciones y procedimientos que cumplen una series de funciones con el fin de ser usados por otro software añadiéndoles una capa de abstracción.
  • WebServices API: son APIs a las cuales solo se puede acceder por medio de internet. Normalmente bajo el protocolo HTTP/S.
  • HTTP: es un protocolo de comunicación que permite la transferencia de información a través de Internet.
  • REST: es un sistema nuevo lanzado como protocolo de intercambio y manipulación de datos en los servicios de Internet que utiliza HTTP. Es diseñado para ser rápido tanto en el desarrollo como en su adopción.
  • RESTFUL: es cualquier servicio web o Webservices API que está basado en Rest y que, además, accede a recursos usando enteramente el protocolo HTTP.

¿Qué es Rest Assured?

Es un Framework escrito en Java y diseñado para simplificar las pruebas sobre servicios basados en REST.

Ofrece un DSL descriptivo (lenguajes específicos de dominio), que muestra una unión a un punto de conexión HTTP y da los resultados esperados. Este framework soporta las operaciones POST, GET, PUT, DELETE, OPTIONS, PATCH y HEAD y contiene herramientas para invocarlas y verificarlas.

¿Por qué debemos usar Rest Assured? 

Rest Assured es utilizado para las pruebas funcionales de servicios ‘REST’, y nos da la posibilidad de validar las respuestas HTTP recibidas del servidor.

Actualmente, sus principales ventajas son:

  • Se puede comprobar el estado del código, del mensaje e, incluso, se puede ver el cuerpo de la respuesta. Además, resulta muy fácil concatenar llamadas y, en todo momento, se tiene el control del código.
  • Son fáciles de integrar con pruebas de todo tipo: funcionales, unitarias, integradas, etc.
  • Totalmente codificado en Java e integrable con cualquier librería/framework de pruebas por separado como jUnit, TestNG o con Maven/Gradle.
  • Se puede integrar fácilmente con Jenkins.
  • Es posible combinar con pruebas automatizadas de UI y no requiere de herramientas externas para ejecutarse.

Ejemplo del diseño y desarrollo de un plan de pruebas automatizado

Paso 0: diseñar las pruebas

Vamos a consumir y a verificar distintos servicios web a modo de ejemplo desde https://reqres.in/ . Estos serán una petición GET, POST, PUT y Delete, y para consumirlos se utilizará el software cURL.

No obstante, antes hay que diseñar un plan de pruebas con dos verificaciones: una de status de la respuesta HTTP y otra con una serie de comprobaciones sobre el cuerpo de la respuesta.

Una vez realizado, hay que probarlos con cURL y finalmente automatizarlos con rest-assured.

Plan de pruebas

PeticiónHTTP StatusValoresDescripción
GET /users?page=2200Tiene un total de 12 elementos y contiene los IDs: 7,8,9,10.11 y 12 en el array de IDs retornado por la petición.Se lista a los usuarios
POSTS/users 201Se añade y se tendría que ver en la respuesta el mismo valor con la fecha de inserción (createdAt) y el ID creado. (id) en el JSON de la respuesta.Se crea un usuario
PUT /users/200Existe una fecha de actualización ( updatedAt) y devuelve los nuevos valores que se han modificado.Se modifica un usuario
DELETE /users/204No se devuelve un JSON la respuesta 204 indica que todo está OK pero no hay contenido.Se elimina el usuario

Petición GET

Se consultará una lista de usuarios. En todas las peticiones se añadirá el parámetro -k que indica que ignore los errores de SSL del certificado del website.

curl -k -H "Accept: application/json" -H "Content-Type: application/json"  https://reqres.in/api/users?page=2

Respuesta:

{
  "page": 2,
  "per_page": 6,
  "total": 12,
  "total_pages": 2,
  "data": [
    {
      "id": 7,
      "email": "michael.lawson@reqres.in",
      "first_name": "Michael",
      "last_name": "Lawson",
      "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/follettkyle/128.jpg"
    },
    {
      "id": 8,
      "email": "lindsay.ferguson@reqres.in",
      "first_name": "Lindsay",
      "last_name": "Ferguson",
      "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/araa3185/128.jpg"
    },
    {
      "id": 9,
      "email": "tobias.funke@reqres.in",
      "first_name": "Tobias",
      "last_name": "Funke",
      "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/vivekprvr/128.jpg"
    },
    {
      "id": 10,
      "email": "byron.fields@reqres.in",
      "first_name": "Byron",
      "last_name": "Fields",
      "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/russoedu/128.jpg"
    },
    {
      "id": 11,
      "email": "george.edwards@reqres.in",
      "first_name": "George",
      "last_name": "Edwards",
      "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/mrmoiree/128.jpg"
    },
    {
      "id": 12,
      "email": "rachel.howell@reqres.in",
      "first_name": "Rachel",
      "last_name": "Howell",
      "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/hebertialmeida/128.jpg"
    }
  ],
  "ad": {
    "company": "StatusCode Weekly",
    "url": "http://statuscode.org/",
    "text": "A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things."
  }
}

Petición POST

Se añade un usuario. La respuesta del servidor que será un HTTP 201 (luego lo revisaremos usando rest-assured), y además nos responderá con el HTTP correcto.

curl -k -d '{"name":"juan", "job":"leader"}' -H "Content-Type: application/json" -X POST https://reqres.in/api/users

Respuesta:

{"name":"juan","job":"leader","id":"667","createdAt":"2020-08-21T05:42:34.102Z"}     

Petición PUT

Se modifica el recurso. En este caso utilizaremos el id creado en la petición anterior 667:

curl -k -d '{"name":"juan", "job":"developer"}' -H "Content-Type: application/json" -X PUT https://reqres.in/api/users/667

Respuesta:

{"name":"juan","job":"developer","updatedAt":"2020-08-21T05:51:04.599Z"}

Petición DELETE

Se elimina el usuario creado y modificado anteriormente. En la respuesta tendríamos que ver una respuesta HTTP 204:

curl  -k -H "Content-Type: application/json" -X DELETE  https://reqres.in/api/users/667

Respuesta:

No nos debería saltar ningún error. No obstante, para comprobar que contiene un ‘HTTP/1.1 204 No Content’ se debería de añadir al comando ” –verbose”. El HTTP status lo comprobaremos usando rest-assured.

Paso 1: Integrar rest-assured a una prueba maven

Para implementar estas pruebas vamos a utilizar JUnit (como runner) y rest-assured, ambos como dependencias maven en el pom.xml.

Si se prefiere usar Gradle como gestor de proyectos se puede obtener más información en la documentación oficial aquí.

Las dependencias son las siguientes:


        
            junit
            junit
            4.12
            test
        
        
            io.rest-assured
            rest-assured
            4.3.1
            test
        
        
            io.rest-assured
            json-path
            4.3.1
            test
        

En src/test/ crearemos una clase llamada rest-assured con un ‘método before’ para establecer de manera estática la URL del API y un método por cada prueba. En nuestro caso serían GET, POST, PUT y DELETE; y quedarían de esta manera:

package org.sdos.blog_rest_assured;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.path.json.JsonPath;
import org.junit.Before;
import org.junit.Test;
 
public class RestTest {
 
    @Before
    public void before(){
        RestAssured.baseURI = "https://reqres.in/api/";
    }
     
    @Test
    public void getTest(){}
 
    @Test
    public void postTest(){}
     
    @Test
    public void putTest(){  }
     
    @Test
    public void delete(){}
}

Tal y como se observa, podemos invocar a rest-assured de manera estática para establecer valores y configuraciones de todas las llamadas como, por ejemplo, un proxy o también que ignore los certificados SSL.

RestAssured.useRelaxedHTTPSValidation(); //ignorar SSL
RestAssured.proxy("localhost");//añadir un proxy
RestAssured.proxy(8888); // añadir un puerto al proxy

En este caso vamos a consumir servicios REST que aceptan y devuelve un JSON, pero se pueden hacer mucho más como, por ejemplo, servicios XML, verificar DTD, etc. Para más información se puede buscar en la guía de uso aquí.

Paso 2: Invocar los servicios

GET

Rest Assured se puede utilizar de muchas maneras, en este caso se utiliza con la sintaxis given() – then() donde:

  1. given() → configurará la cabecera y se enviará la petición.
  2. then() → se tratará la respuesta y se aplicarán los Assertions o verificaciones del código, de manera que:

una llamada GET a /users?page=2 y el tratamiento de su respuesta queda así:

given()
    .param("page", 2)
    .get("/users");

Implementación de acuerdo al plan de pruebas:

PeticiónHTTP StatusValores
GET  /users?page=2200 (*1)Tiene un total de 12 elementos (*2) Contiene los IDs: 7,8,9,10,11 y 12 en el array de IDs retornado en la petición (*3)
given().param("page", 2)
    .get("/users")
.then()
    .statusCode(200)      // (*1)
    .body("total_pages", equalTo(2))    //(*2)
    .and()
    .body("data.id",hasItems(7,8,9,10,11,12) ) //(*3)
    ;

Se observa que existen verificaciones simples como .statusCode(200) que fallarían si se devuelve un código distinto y, además, también existen verificaciones de esta forma: body (JSONPATH, verificador).and().body(JSONPATH, verificador) ….

Para el acceso y ‘parseo’ de elementos se usa JSONPath. Esto facilita el acceso a elementos cuando estamos navegando por un JSON.

Rest-assured utiliza la sintaxis de Groovy para esto (Ver aquí), de igual manera que si nuestro servicio fuese un XML lo podríamos ‘parsear’ con XPath.

Finalmente usando JSONPath data.id hace referencia en el documento a una lista de ID donde finalmente se realiza una comprobación con un verificador o matcher predefinido: hasItems(). (con este se verifica si esos elementos aparecen en la lista).

Rest-assured se ayuda de la biblioteca Hamscrest (tutorial aquí), que contiene un gran número de matchers predefinidos.

Por defecto rest-assured no muestra nada por pantalla, pero eso se puede solucionar añadiendo delante ‘given()’ para el header y ‘then()’ para la respuesta. El método log() se muestra por pantalla y se puede configurar en varios niveles: siempre, si la verificación falla, si salta algún error, etc.

Una vez realizado, el método GET quedaría así:

@Test
    public void getTest(){
        given().log().all().param("page", 2)
            .get("/users")
        .then().log().ifError()
            .statusCode(200)
            .body("total_pages", equalTo(2))
            .and()
            .body("data.id",hasItems(7,8,9,10,11,12) )
            ;
    }

Quedando la salida de esta manera:

$>  mvn clean test
 
 
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running org.sdos.blog_rest_assured.RestTest
Request method: GET
Request URI:    https://reqres.in/api/users?page=2
Proxy:          
Request params: page=2
Query params:   
Form params:    
Path params:    
Headers:        Accept=*/*
Cookies:        
Multiparts:     
Body:           
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.838 sec
 
Results :
 
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

POST

La llamada POST quedaría de esta manera, donde se verifica que existe en la respuesta un campo con ID y con createdAt, además de la respuesta HTTP 201:

PeticiónHTTP StatusValoresDescripción
POST /users {“name”:”juan”,”job”:”leader”}201 (*1)Se añade y se tendría que ver en la respuesta el mismo valor con la fecha de inserción (createdAt) y el ID creado. (id) en el JSON de la respuesta. (*2)Se crea un usuario
@Test
    public void postTest(){
        given()
            .accept(ContentType.JSON)
            . body("{"name":"juan", "job":"developer"}")
            .post("/users")
        .then().log().ifValidationFails()
            .statusCode(201)             //(*1)
            .body("$", hasKey("id"))     //(*2)
            .and()
            .body("$", hasKey("createdAt"))//(*2)
        ;
    }

PUT

Para la llamada PUT vamos a realizar una práctica muy común en la automatización de pruebas y es crear los datos de prueba, esto con rest-assured es relativamente sencillo, se puede hacer de muchas maneras incluso concatenando llamadas, no obstante vamos a seguir estos puntos:

  1. Invoca a una llamada HTTP y se iguala a un objeto JsonPath de esta forma: JSONPath jsonPath= given()….then().extract().jsonPath()
  2. Una vez tengo el JSONPath y teniendo en cuenta que el JSON de la respuesta es de la forma {id=20,name=xxx, … } se obtiene el elemento id de la siguiente llamada invocando a jsonPath().get(“id”).toString().
  3. Se utiliza el dato/id obtenido de punto 2 para la siguiente llamada.

Al final la llamada PUT quedaría de esta forma:

  1. Se hace una llamada POST para crear el dato a modificar
  2. Se hace una llamada PUT con el dato a modificar y se verifica que se ha modificado, verificando el dato modificado y si la respuesta tiene el campo updatedAt además de una respuesta HTTP 200
PeticiónHTTP StatusValoresDescripción
PUT/users/200 (*1)Existe una fecha de actualización (updatedAt). (*2) Se devuelven los nuevos valores que se han modificado. (*3)Se modifica un usuario
@Test
public void putTest(){
     
    // Se crea el dato de prueba
    JsonPath jsonPath  = given()
        .accept(ContentType.JSON)
        . body("{"name":"juan2", "job":"leader"}")
        .post("/users")
    .then()
        .extract().jsonPath();
    ;
     
    // Se extrae el dato de la llamada
    String idCreated =  jsonPath.get("id").toString() ;
     
                     
     given()
    .log().all()
        .accept(ContentType.JSON)
        .contentType(ContentType.JSON)
        . body("{ "job" : "TESTER" }")
        .put("/users/"+idCreated)       //  <-- se utiliza el dato creado en el POST
    .then().log().all()
        .statusCode(200)                //  (*1)
        .and()
        .body("$", hasKey("updatedAt")) //  (*2)
        .body("job", equalTo("TESTER")) //  (*3)
        ;
     
     
}

Quedando de esta manera la salida:

Running org.sdos.blog_rest_assured.RestTest
Request method: PUT
Request URI:    https://reqres.in/api/users/827
Proxy:          
Request params: 
Query params:   
Form params:    
Path params:    
Headers:        Accept=application/json, application/javascript, text/javascript, text/json
                Content-Type=application/json; charset=UTF-8
Cookies:        
Multiparts:     
Body:
{
    "job": "TESTER"
}
HTTP/1.1 200 OK
Date: Thu, 15 Oct 2020 06:45:01 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: __cfduid=dd000ed063620fd1965063e0a36886ff21602744301; expires=Sat, 14-Nov-20 06:45:01 GMT; path=/; domain=.reqres.in; HttpOnly; SameSite=Lax; Secure
X-Powered-By: Express
Access-Control-Allow-Origin: *
Etag: W/"37-C58imJupqE/gDYMtIjDXjTrTdHk"
Via: 1.1 vegur
CF-Cache-Status: DYNAMIC
cf-request-id: 05cc9a4f0500001121d2b55000000001
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
Report-To: {"endpoints":[{"url":"https://a.nel.cloudflare.com/report?lkg-colo=40&lkg-time=1602744301"}],"group":"cf-nel","max_age":604800}
NEL: {"report_to":"cf-nel","max_age":604800}
Vary: Accept-Encoding
Server: cloudflare
CF-RAY: 5e27932b39cc1121-MAD
Content-Encoding: gzip
 
{
    "job": "TESTER",
    "updatedAt": "2020-10-15T06:45:01.378Z"
}

DELETE 

PeticiónHTTP StatusValoresDescripción
DELETE/users/204No se devuelve un JSON la respuesta 204 indica que todo está OK, pero no hay contenido. (*1)  Se elimina el usuario

Para el método delete también de autogenerará los datos de prueba mediante POST, y acto seguido invocar al método DELETE. El código sería el siguiente:

 @Test
public void delete(){
     
    //se crea el recurso
    JsonPath jsonPath  = given().log().all()
            .accept(ContentType.JSON)
            . body("{"name":"juan2", "job":"leader"}")
            .post("/users")
        .then()
            .extract().jsonPath();
        ;
         
        String idCreated = ( jsonPath.get("id").toString() );
     
    //se elimina el recurso
    given().log().all()
        .accept(ContentType.JSON)
        .param("id", idCreated)
        .delete("/users")
    .then().log().ifValidationFails()
        .statusCode(204);             // (*1)
}

Salida:

Running org.sdos.blog_rest_assured.RestTest
Request method: POST
Request URI:    https://reqres.in/api/users
Proxy:          
Request params: 
Query params:   
Form params:    
Path params:    
Headers:        Accept=application/json, application/javascript, text/javascript, text/json
                Content-Type=text/plain; charset=ISO-8859-1
Cookies:        
Multiparts:     
Body:
{"name":"juan2", "job":"leader"}
Request method: DELETE
Request URI:    https://reqres.in/api/users?id=478
Proxy:          
Request params: id=478
Query params:   
Form params:    
Path params:    
Headers:        Accept=application/json, application/javascript, text/javascript, text/json
Cookies:        
Multiparts:     
Body:           

Conclusiones

Hay muchas herramientas como Postman o SoapUI para realizar API Testing sobre un Restful.

A continuación enumeramos una serie de ventajas que nos hacen escoger esta herramienta:

  • 'Reusabilidad' de código: al estar escrito en Java y no estar en ningún Entorno integrado podemos integrarla fácilmente con automatizaciones de UI o diseñar un Framework para reutilizar/mantener mejor el código.
  • El diseñar un Framework usando data-driven: al estar escrito en Java y en un lenguaje orientado a objetos podemos diseñar muchas fuentes de datos como Base de datos, ficheros XML/JSON usando 'serialización' nativa de la misma biblioteca, etc.
  • CI Integration: al solo necesitar Java para ejecutarse es muy fácil su integración con Jenkins o cualquier herramientas de CI o incluso 'dockerizar' sus lanzamientos.

Algunas desventajas:

  • Reporting: no tiene ninguna herramienta de reporting integrada frente a SoapUI o Postman, no obstante también esto se puede ver de otra manera y es que podemos integrar RestAssured con cualquier herramienta/Biblioteca existente.
  • Programación en Java: al estar en un DSL (Domain specific Languaje) lo que puede parecer una ventaja, hace que tenga una curva de aprendizaje mayor que otras herramientas.

¿Qué os parece este framework? ¿Lo vais a poner en práctica? ¡En ALTEN nos encantaría saber vuestra opinión y experiencia personal!

Jaime Rodríguez,
QA Service Manager