Testing y documentación de servicios REST

Post on 15-Dec-2014

356 views 2 download

description

Durante esta sesión se abordarán distintos enfoques a la hora de realizar pruebas de nuestros servicios REST.Utilizando la implementación de referencia del estándar Java JAX-RS, se presentarán distintas opciones para poder cargar nuestro entorno, ejecutar nuestras pruebas y gestionar su ejecución en entornos de integración contínua.Por otra parte, se presentarán distintas herramientas que nos permitan elaborar una documentación sencilla y mantenible de nuestras APIs públicas.

Transcript of Testing y documentación de servicios REST

TESTING Y DOCUMENTACIÓN DE SERVICIOS

REST@BORILLO

YO { name : 'Ricardo Borillo', company : 'Universitat Jaume I', mail : 'borillo@uji.es', social : { twitter : '@borillo', blog : 'xml-utils.com', linkedin : 'linkedin.com/in/borillo' } }

YO

ÍNDICE

HTTP y RESTJersey JAX-RSTesting: Objetivos y alternativasMejora de la expresividadDocumentación de los servicios

HTTP Y REST

CARACTERÍSTICAS DE REST:

USO DE LOS VERBOS HTTP

CARACTERÍSTICAS DE REST:

CUALQUIER FORMATO SOBRE HTTP

CARACTERÍSTICAS DE REST:

ORIENTADO A RECURSOSLista todos los coches o recupera uno:

Añade, modifica o elimina un coche:

GET /carsGET /cars/1234AAW

POST /carsPUT /cars/1234AAWDELETE /cars/1234AAW

LA GRAN VENTAJA DE REST

APROVECHA AL MÁXIMO LAINSFRASTRUCTURA DE HTTP

Simplicidad, escalabilidad, cacheo, seguridad, ...

REST != RPCEvitar cosas como:

Utilizar nombres que definen recursos:

/getUsuario/getAllAsuarios/modificaCuentaById

/usuarios/usuarios/1/usuarios/1/facturas

JERSEY JAX-RS

¿QUÉ ES?Jersey es la implementación Java de referencia del

estándar JAX-RS para la definición de servicios REST:https://jersey.dev.java.net/

CONFIGURACIÓN DE UNA APLICACIÓN WEB JERSEYUsando Maven:

<dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-server</artifactId> <version>1.17.1</version></dependency>

<dependency> <groupId>com.sun.jersey</groupId> <artifactId>jersey-servlet</artifactId> <version>1.17.1</version></dependecy>

¿QUÉ ES?Mapear peticiones HTTP a código Java

@GET / @POST / @PUT / @DELETE@Path("users")public class UsersResource { @GET public List<User> getUsers() { ... }}

GET /users

¿QUÉ ES?Mapear parámetros de URL a parámetros de entrada a

los métodos

@PathParam / @QueryParam@GET@Path("/users/{userId}")public User getUser( @PathParam("userId") String userId, @QueryParam("debug") @DefaultValue("5") String debug) { }

GET /users/1421?debug=S

¿QUÉ ES?Declaración del formato de los contenidos recibidos o

emitidos

@Consumes / @Produces@GET@Produces(MediaType.APPLICATION_XML)public List<User> getUsers() { }

@PUT@Consumes(MediaType.APPLICATION_JSON)public void updateUser(User user) { }

MAPEO DE LA PETICIÓN HTTP:

MAPEO DE LA RESPUESTA HTTP:

OTRAS FUNCIONALIDADES DISPONIBLES:HypermediaSeguridad: OAuth, SSL, etcLoggingGestión de excepcionesSoporte para Spring FrameworkAPI de acceso clienteUploads: Jersey MultipartTesting: Jersey Test FrameworkY mucho más ...

Servicios REST: Jersey JAX-RShttps://vimeo.com/53338309

CÓDIGO DE EJEMPLO:

https://github.com/borillo/template-jersey-spring-jpa

TESTING:

OBJETIVOS BÁSICOS

¿QUÉ NOS GUSTARÍA CONSEGUIR?Expresividad y sencillez en las validacionesEntorno integradoArraque automático de los servicios desarrolladosEjecución automática de las pruebasRestitución del entorno

TESTING:

APROXIMACIÓN INICIAL

HTTP: ACCEDIENDO A CONTENIDOS EN LA WEBA través de un navegador web:

Consulta de páginasEnvío de formulariosSubir ficheros al servidor

HTTP: ACCEDIENDO A CONTENIDOS EN LA WEBDesde línea de comandos

O extrayendo las peticiones de las Chrome Tools

curl -XGET http://www.google.es/

HTTP: ACCEDIENDO A CONTENIDOS EN LA WEB

REST SHELLConsola sencilla de utilizar y con completadoCompatible con HATEOAS (soporta discover)Fácil interacción con servicios RESTCarga y guardado de peticiones y respuestasConfiguración del contexto: Cabeceras, auth, etc

Disponible en: GitHub REST-shell

HTTP: ACCEDIENDO A CONTENIDOS EN LA WEBDefinición del recurso a utilizar:

Acceso a recursos:

http://localhost:8080:> baseUri http://xxxxxxx

Base URI set to 'http://xxxxxxx'

> get resource --params "{ param1 : 'value' }"

> post resource --data "{ param1 : 'value' }"

> post --from data.json

HTTP: ACCEDIENDO A CONTENIDOS EN LA WEBHATEOAS: discover:

> discover

rel href========================================================people http://localhost:8080/person

> follow people

http://localhost:8080/person:> list

rel href===================================================people.Person http://localhost:8080/person/1people.search http://localhost:8080/person/search

HTTP: ACCEDIENDO A CONTENIDOS EN LA WEBHATEOAS: get

http://localhost:8080/person:> get 1> GET http://localhost:8080/person/1

< 200 OK< ETag: "2"< Content-Type: application/json<{ "links" : [ { "rel" : "self", "href" : "http://localhost:8080/person/1" }], "name" : "John Doe"}

HTTP: ACCEDIENDO A CONTENIDOS EN LA WEBDesde algún lenguaje de programación como Java

O con el API cliente de Jersey JAX-RS:

DefaultHttpClient client = new DefaultHttpClient();client.execute(new HttpGet("http://www.uji.es/"));

Client client = Client.create();WebResource resource = client.resource("http://www.uji.es/");ClientResponse response = resource.accept("text/html"). get(ClientResponse.class);if (response.getStatus() == 200) { System.out.println(response.getEntity(String.class));}

TESTING:

HERRAMIENTAS DEAUTOMATIZACIÓN

SOAPUI

CREACIÓN DEL PROYECTO

SOAPUI

EJECUCIÓN DEL SERVICIO

SOAPUI

CREACIÓN DEL TESTCASE

SOAPUI

EJECUCIÓN DEL TEST

SOAPUI

AÑADIR UNA ASERCIÓN

SOAPUI

VALOR EXPECTED DE LA ASERCIÓN

SOAPUI

INFORME DE EJECUCIÓN DE LA SUITE

SOAPUI

INFORME DE EJECUCIÓN DE LA SUITE

TESTING:

JERSEY TESTFRAMEWORK

DEPENDENCIAS EXTRA NECESARIASAñadir al pom.xml las siguientes dependencias:

<dependency> <groupId>com.sun.jersey.jersey-test-framework</groupId> <artifactId>jersey-test-framework-core</artifactId> <version>1.17.1</version></dependency>

<dependency> <groupId>com.sun.jersey.jersey-test-framework</groupId> <artifactId>jersey-test-framework-grizzly</artifactId> <version>1.17.1</version></dependency>

DEFINICIÓN DE UN TESTCódigo necesario para arrancar el contenedor Java:public class UsersResourceTest extends JerseyTest { private WebResource resource;

public UsersResourceTest() { super(new WebAppDescriptor.Builder("com.decharlas.services") .contextParam("webAppRootKey", "jersey-maven.root") .servletClass(ServletContainer.class).build()); this.resource = resource(); }

@Override protected TestContainerFactory getTestContainerFactory() { return new GrizzlyWebTestContainerFactory(); }}

DEFINICIÓN DE UN TESTDefinición de los tests:

public class UsersResourceTest extends JerseyTest {

// Métodos de definición de la slide anterior

@Test public void deleteUser() throws Exception { ClientResponse response = resource.path("users/1") .accept("application/json") .delete(ClientResponse.class);

Assert.assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); }}

CÓDIGO DE EJEMPLO:

https://github.com/borillo/template-jersey-spring-jpa

MEJORANDO LAEXPRESIVIDAD DENUESTROS TESTS

OBJETIVOSLas aserciones en jUnit no resultan nada semánticas:

Vamos a ver como mejorarlas y así conseguir:

Aserciones más fáciles de leerMenos duplicación de código en las pruebasMejora de la semánticaFacilidad de comprobación de los resultados

Assert.assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());

MEJORANDO LA EXPRESIVIDAD DENUESTROS TESTS:

HAMCREST

MEJORAR LOS TESTS CON HAMCRESTMatchers para asserts sobre nuestros servicios

public class OkResponseMatcher extends TypeSafeMatcher<ClientResponse> { @Override public boolean matchesSafely(ClientResponse response) { return (response != null && response.getStatus() == 200); }

public void describeTo(Description description) { description.appendText("not a HTTP 200 response"); }

@Factory public static <T> Matcher<ClientResponse> ok() { return new OkResponseMatcher(); }}

MEJORAR LOS TESTS CON HAMCRESTUso del anterior matcher "OkResponseMatcher"@Testpublic void test() { ClientResponse response = resource.path("users/1").get(ClientResponse.class);

...

assertThat(response, is(ok()));}

MEJORANDO LA EXPRESIVIDAD DENUESTROS TESTS:

REST-ASSURED

DEFINICIÓN

Testing and validating REST services in Java is harderthan in dynamic languages such as Ruby and Groovy.

REST Assured brings the simplicity of using theselanguages into the Java domain.

DEPENDENCIAS EXTRA NECESARIASAñadir al pom.xml las siguientes dependencias:

<dependency> <groupId>com.jayway.restassured</groupId> <artifactId>rest-assured</artifactId> <version>1.8.0</version> <scope>test<scope></dependency>

CARACTERÍSTICAS PRINCIPALES:Integración HTTP total: Cookies, auth, headers, ...Expresivo DSL para realizar las comprobacionesJsonPath y XmlPath para validar informaciónMatchers Hamcrest para las validacionesCustom parsers por content typeDispone de un StubServerLoggingMucho más!! Consulta la doc oficial :)

TEST: EL USERID DEBE SER 5 { "User": { "userId": 5, "friends": [{ "userId": 23, "refs": [2, 45, 34, 23, 3, 5] }, { "userId": 54, "refs": [52, 3, 12, 11, 18, 22] }] }}

given().expect().body("User.userId", equalTo(5)). when().get("/users");

TEST: 23 Y 54 DEBEN ESTAR ENTRE LOS AMIGOS { "User": { "userId": 5, "friends": [{ "userId": 23, "refs": [2, 45, 34, 23, 3, 5] }, { "userId": 54, "refs": [52, 3, 12, 11, 18, 22] }] }}

expect().body("User.friends.userId", hasItems(23, 54)). when().get("/users");;

TEST: EL SALUDO DEBE SER PARA RICARDO<greeting> <firstName>Ricardo</firstName> <lastName>Borillo</lastName></greeting>

expect().body(hasXPath("//firstName", containsString("Ricardo"))). when().post("/greets");

expect().body(hasXPath("//firstName[text()='Ricardo']")). when().post("/greets");

INTEGRACIÓN CON JERSEYJersey Test Framework: Arranque servicios REST conGrizzlyConfiguramos REST-assured para conectar a estosserviciosRestAssured.baseURI = "http://localhost";RestAssured.port = this.resource.getURI().getPort();RestAssured.basePath = "/appbasepath";RestAssured.authentication = basic("username","password");

CÓDIGO DE EJEMPLO:

https://github.com/borillo/template-rest-assured

DOCUMENTACIÓN DE SERVICIOS REST:

SWAGGER

¿QUÉ ES SWAGGER?Swagger is a specification and complete framework

implementation for describing, producing, consuming,and visualizing RESTful web services.

The overarching goal of Swagger is to enable client anddocumentation systems to update at the same pace as the

server. With Swagger, deploying managing, and usingpowerful APIs has never been easier.

SWAGGER-UI: EL INTERFAZ DE SWAGGERConjunto de ficheros HTML/CSS/JavaScript sinninguna dependencia adicionalDocumentación atractiva y dinámicaPermite la interacción con los servicios RESTSólo es necesario que nuestros servicios REST sean"swagger-compliant"

SWAGGER-UI: APIS REST "SWAGGER COMPLIANT"Los servicios REST deben exportar su descripción{ apiVersion: "0.2", swaggerVersion: "1.1", basePath: "http://petstore.swagger.wordnik.com/api", apis: [ { path: "/pet.{format}", description: "Operations about pets" }, { path: "/user.{format}", description: "Operations about user" } ]}

SWAGGER-UI: POSIBILIDADES DE INTEGRACIÓNDisponibles integraciones para múltiples lenguajes yframeworks:

Java/Scala JAX-RSNodeJSGrailsSymfony 2Muchos más ...

SWAGGER-UI: GENERACIÓN DEL CLIENTE HTMLDescargamos el código del proyecto:

Inicializamos dependencias y construimos:

En el directorio dist tenemos el UI listo para copiar adonde queramos

git clone https://github.com/wordnik/swagger-ui.git

npm installnpm run-script build

SWAGGER:

INTEGRACIÓN CONJERSEY JAX-RS

DEPENDENCIAS EXTRA NECESARIASAñadir al pom.xml las siguientes dependencias:

<dependency> <groupId>com.wordnik</groupId> <artifactId>swagger-jaxrs_2.9.1</artifactId> <version>1.2.1</version></dependency>

CARGA DEL PROVIDER DE SWAGGER EN JERSEYModificar la definición de Jersey en el web.xml:

<servlet> <servlet-name>jersey</servlet-name> <servlet-class> com.sun.jersey.spi.container.servlet.ServletContainer </servlet-class> <init-param> <param-name> com.sun.jersey.config.property.packages </param-name> <param-value> com.your.project; com.wordnik.swagger.jaxrs.listing </param-value> </init-param> ...</servlet>

PARÁMETROS BÁSICOS DE SWAGGERModificar la definición de Jersey en el web.xml:

<servlet> ... <init-param> <param-name>swagger.api.basepath</param-name> <param-value>http://localhost:8080</param-value> </init-param> <init-param> <param-name>api.version</param-name> <param-value>1.0</param-value> </init-param> ...</servlet>

ANOTACIONES EN LOS SERVICIOS RESTAnotaciones Swagger en nuestro servicios Jersey:

@Path("/pet.json")@Api(value = "/pet", description = "Operations about pets")@Produces({"application/json"})public class PetResource { @GET @Path("/{petId}") @ApiOperation(value="Find pet", notes="Extra notes", responseClass="com.model.Pet") @ApiErrors(value={@ApiError(code=400, reason="Invalid ID supplied"), @ApiError(code=404, reason="Pet not found")}) public Response getPetById ( @ApiParam(value="Pet ID", required=true) @PathParam("petId") String petId) throws NotFoundException { // your resource logic } ...}

DOCUMENTACIÓN GENERADAAccedemos al índice de servicios documentados:

curl -XGET http://localhost:8080/api-docs.json

{ apiVersion: "1.0", swaggerVersion: "1.0", basePath: "http://localhost:8080", apis: [ { path: "/api-docs.{format}/pet", description: "Operations about pets" } ]}

DOCUMENTACIÓN GENERADAY luego a la descripción de un servicio:

curl -XGET http://localhost:8080/api-docs.json/pet

{ apiVersion: "1.0", swaggerVersion: "1.0", basePath: "http://localhost:8080", resourcePath: "/pet", apis: [ { path: "/pet.{format}/{petId}", description: "Operations about pets", operations: [ { parameters: [ { name: "petId", ...

CÓDIGO DE EJEMPLO:

https://github.com/borillo/template-jersey-swagger

SWAGGER:

INTEGRACIÓN CONNODEJS & EXPRESS

CONFIGURACIÓN DE SWAGGER-UICopiamos swagger-ui al proyecto y lo inicializamos:var docs_handler = express.static(__dirname + '/swagger-ui/');

app.get(/̂\/docs(\/.*)?$/, function(req, res, next) { if (req.url === '/docs') { res.writeHead(302, { 'Location' : req.url + '/' }); res.end(); return; }

req.url = req.url.substr('/docs'.length); return docs_handler(req, res, next);});

DESCRIPCIÓN DE LOS MODELOS DE LA APLICACIÓNFichero models.js:

exports.models = { "User": { "id": "User", "properties": { "id": { "type":"long" }, "name": { "type":"string" } } }};

DESCRIPCIÓN DE LOS RECURSOS RESTexports.getUserById = { 'spec': { "description" : "users", "path": "/users.{format}/{userId}", "notes": "Returns an user based on ID", "summary": "Find user by ID", "method": "GET", "params": [param.path("userId", "ID of the fetched user", "string")], "responseClass": "User", "errorResponses": [ swaggerErrors.invalid('id'), swaggerErrors.notFound('user') ], "nickname": "getUserById" }, 'action': function (req,res) { // procesamiento }};

CONFIGURAMOS LOS PARÁMETROS DE SWAGGERModelos, recursos, punto de acceso y versión:

var swagger = require("./swagger.js"), resources = require("./resources.js"), models = require("./models.js");

var app = express();app.use(express.bodyParser());

swagger.setAppHandler(app);swagger.addModels(models).addGet(resources.getUserById);swagger.configure("http://localhost:8002", "0.1");

CÓDIGO DE EJEMPLO:

https://github.com/borillo/template-nodejs-swagger

¿PREGUNTAS?