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ón | HTTP Status | Valores | Descripción |
---|---|---|---|
GET /users?page=2 | 200 | Tiene 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 | 201 | 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. | Se crea un usuario |
PUT /users/ | 200 | Existe una fecha de actualización ( updatedAt) y devuelve los nuevos valores que se han modificado. | Se modifica un usuario |
DELETE /users/ | 204 | No 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:
- given() → configurará la cabecera y se enviará la petición.
- 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ón | HTTP Status | Valores |
---|---|---|
GET /users?page=2 | 200 (*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ón | HTTP Status | Valores | Descripció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:
- Invoca a una llamada HTTP y se iguala a un objeto JsonPath de esta forma: JSONPath jsonPath= given()….then().extract().jsonPath()
- 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().
- Se utiliza el dato/id obtenido de punto 2 para la siguiente llamada.
Al final la llamada PUT quedaría de esta forma:
- Se hace una llamada POST para crear el dato a modificar
- 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ón | HTTP Status | Valores | Descripció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ón | HTTP Status | Valores | Descripción |
---|---|---|---|
DELETE/users/ | 204 | No 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