TESTING Y DOCUMENTACIÓN DE SERVICIOS
REST@BORILLO
YO { name : 'Ricardo Borillo', company : 'Universitat Jaume I', mail : '[email protected]', 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
PLUGINS: PRUEBAS DESDE EL NAVEGADOR
REST clientREST ConsolePOSTmanAdvance REST client
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?