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.