TP 5 Serveur REST, tests et premier client REST Xavier de Rochefort xderoche@labri.fr - labri.fr/~xderoche 13 mai 2014 Résumé Les derniers TP vous ont guidé dans ➊ la mise en place d un serveur REST avec Jersey, implémentation de référence de l API JAX-RS, et ➋ l utilisation de l API JAXB et son implémentation MOXy pour la sérialisation d objets Java en XML/JSON et la désérialisation de XML en objets Java. Ce TP vous propose de manipuler quelques mécanismes mis à disposition par Jersey pour automatiser les tests unitaires d un serveur REST. 1 Du TP4 au TP5 Nous partons du projet WSRestService du TP4. Q1.1 Modifier la classe SampleService de manière à ne garder que les 3 méthodes suivantes : addalbum, permettant l ajout d un album par une requête HTTP POST à l URI albums/add/ utilisant les paramètres title, genre, et date et retournant l album ajout (les 2 doivent être possibles). deletealbum, permettant la suppression d un album par une requête HTTP DELETE à l URI albums/del/{id-album getalbums, permettant la récupération de la liste des albums par une requête HTTP GET à l URI albums/list Noter l utilisation standard de la sémantique des méthodes HTTP. L ajout d un album par la méthode POST peut s effectuer par une requête sans corps encodant les paramètres title, genre et date dans l URI. Comme pour récupérer les paramètre d une requête GET, l utilisation de l annotation JAX-RS @QueryParam est possible. Q1.2 Ajouter la possibilité de recevoir les réponses de la requête add et list en JSON. JAX-RS permet de spécifier plusieurs formats de message (XML ou JSON) par le biais de l annotation @Produces. La sérialisation en JSON nécessite la bibliothèque jersey-json. <groupid>com.sun.jersey</groupid> <artifactid>jersey-json</artifactid> <version>1.18.1</version> Enfin, dans le web.xml de la servlet ajouter les balises suivantes pour que le containeur active la fonctionnalité de sérialisation JSON de Jersey. <init-param> <param-name>com.sun.jersey.api.json.pojomappingfeature</param-name> <param-value>true</param-value> </init-param> 1
Q1.3 Créer l interface AlbumService suivante. public interface AlbumService { /** * add an album to the collection and return the new Album instance */ public Album addalbum(string title, String genre, String date); /** * delete the album with id albumid */ public void deletealbum(string albumid); /** * return all album in collection */ public Collection<Album> getalbums(); Q1.4 Créer une classe AlbumServiceTest implémentant l interface AlbumService en y déplaçant et en adaptant la Map<String, Album> albums et le code static de la classe SampleService. public class AlbumServiceTest implements org.rest.service.entities.albumservice { private static Map<String, Album> albums = new HashMap<String, Album>(); private static int id = 1; static { Album album1 = new Album(); album1.setalbumid(string.valueof(++id)); album1.settitle("elephunk"); album1.setgenre("hiphop"); Calendar cal = Calendar.getInstance(); cal.set(2003, 06, 24); album1.setdate(cal.gettime()); albums.put(album1.getalbumid(), album1); // public Album addalbum(string title, String genre, String date) { // public void deletealbum(string albumid) { // public Collection<Album> getalbums() { // Q1.5 Renommer la classe SampleService en AlbumResource et modifier la de manière à déléguer l ajout, la suppression et la récupération des albums à un objet de type AlbumService initialisé dans le constructeur par défaut de la classe. Nous utilisons ici le service de test pour 2
simuler l utilisation d une véritable de données. public class AlbumResource { private AlbumService service; public AlbumResource() { this.service = new AlbumServiceTest(); Q1.6 Lancer le serveur et vérifier le bon fonctionnement des 3 méthodes. Ajoutez la dépendance vers jersey-servlet et le plugin Jetty. <groupid>com.sun.jersey</groupid> <artifactid>jersey-servlet</artifactid> <version>1.18.1</version> <plugin> <groupid>org.eclipse.jetty</groupid> <artifactid>jetty-maven-plugin</artifactid> <version>9.0.5.v20130815</version> </plugin> Renseigner le web.xml pour permettre le déploiement de la servlet. <?xml version="1.0" encoding="utf-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web -app_2_5.xsd" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ web-app_2_5.xsd" id="webapp_id" version="2.5"> <display-name>serveur REST TP5</display-name> <servlet> <servlet-name>musiclibrary REST Service</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>org.rest.service.entities</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>musiclibrary REST Service</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app> Lancer Jetty à l aide de la commande mvn jetty:run. 2 Débuter avec Jersey Test Framework Le TP4 vous demandait de valider le bon fonctionnement de votre service en utilisant la commande curl pour effectuer les requêtes de test manuellement. Une bonne pratique consiste 3
à automatiser cette validation par le biais d une batterie de tests unitaires lancé à chaque évolution du code afin de garantir sa non-régression. L écriture et l automatisation de ces tests dans le cas d un serveur REST impliquent le déploiement des servlets et la génération des requêtes aux différents services. Pour aider le développeur dans cette tâche, Jersey intègre un framework de test. Q2.1 Dans le fichier pom.xml du projet, ajouter les dépendances à JUnit et au framework de test de Jersey. <groupid>junit</groupid> <artifactid>junit</artifactid> <version>4.8.1</version> <scope>test</scope> <groupid>com.sun.jersey.jersey-test-framework</groupid> <artifactid>jersey-test-framework-grizzly</artifactid> <version>1.18</version> <scope>test</scope> Nous utilisons ici une version du framework intégrant le conteneur Grizzly. Cependant, d autres conteneurs, comme Jetty, peuvent être utilisés. Nous avons choisi ici le module jersey-test-framework-grizzly pour sa simplicité de mise en œuvre. Q2.2 Créer un dossier src/test/java, destiné à accueillir nos tests (standard maven), et y ajouter un package org.rest.service.entities. Q2.3 Dans le package nouvellement créé, ajouter une classe AlbumResourceTest héritant de la classe com.sun.jersey.test.framework.jerseytest. Q2.4 Ajouter un constructeur par défaut contenant un appel au constructeur parent prenant en paramètre le nom du package contenant le service testé. public AlbumResourceTest() { super("org.rest.service.entities"); 4
Q2.5 Ajouter une classe d adaptation du client Jersey REST pour faciliter l écriture des tests. public class AlbumTestClient { private WebResource webresource; public AlbumClient(WebResource resource) { webresource = resource; public List<Album> get() { GenericType<Collection<Album>> generictype = new GenericType<Collection<Album>>(){; return (List<Album>) webresource.path(albumresource.path + AlbumResource.PATH_GET).get( generictype); public ClientResponse del(string id) { /* */ public ClientResponse add(string title, String genre, String date) { /* */ Q2.6 Dans la classe de test, utiliser l annotation @Before de JUnit pour instancier ce client nouvellement créé dans une méthode d initialisation des tests. @Before public void init() { this.client = new AlbumTestClient(super.resource()); Q2.7 AJouter un premier test JUnit testant la méthode albums/get du service. @Test public void testgetalbums() { List<Album> albums = (List<Album>) client.get(); assertnotnull(albums); assertequals(2, albums.size()); for(album a : albums) System.out.println(a.getAlbumId() + " " + a.gettitle() + " " + a.getgenre() + " " a. getdate()); Q2.8 Lancer la phase test de maven (commande mvn test). 3 Demon Days HipHop Fri Jan 06 00:42:09 CET 2006 2 Elephunk HipHop Thu Jul 24 00:42:09 CEST 2003 Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.804 sec 5
3 Codes HTTP, erreurs et exceptions Les codes retours du protocole HTTP ont une large gamme sémantique 1 qu il est recommandé d utiliser pour rester dans les standards d échanges. Par exemple, en cas de requêtes contenant des paramètres erronés, il est plus cohérent de renvoyer un message HTTP avec un code BAD REQUEST que n importe quel client HTTP peut comprendre, qu un message portant le code OK dont le corps contient un format de message d erreur spécifique au service que le client devra désérialiser pour l interpréter. Appliquons ce principe à la gestion du cas où une requête de suppression d album s accompagne d un identifiant inexistant dans la liste de notre service. Q3.1 Créer une classe AlbumNotFoundException héritant de la classe java.lang.exception et implémentant l interface java.io.serializable. public class AlbumNotFoundException extends Exception implements Serializable { private static final long serialversionuid = 1L; public AlbumNotFoundException() { super(); public AlbumNotFoundException(String msg) { super(msg); public AlbumNotFoundException(String msg, Exception e) { super(msg, e); Q3.2 Jersey permet d associer automatiquement un type d exception à un gestionnaire par l interface ExceptionMapper et à l annotation @Provider. Créer une classe générant une réponse de type BAD REQUEST contenant le message d erreur de l exception lorsqu une AlbumNotFoundException est levée. import javax.ws.rs.core.response; import javax.ws.rs.core.response.status; import javax.ws.rs.ext.exceptionmapper; import javax.ws.rs.ext.provider; @Provider public class ApplicationExceptionHandler implements ExceptionMapper<AlbumNotFoundException> { public Response toresponse(albumnotfoundexception exception) { return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build(); 1. http://fr.wikipedia.org/wiki/liste_des_codes_http 6
Q3.3 Modifier la déclaration de la méthode deletealbum de l interface AlbumService pour indiquer la levée éventuelle d une exception AlbumNotFoundException lors de son appel. public void deletealbum(string albumid) throws AlbumNotFoundException; Q3.4 Modifier en conséquence la classe AlbumServiceTest et faire en sorte qu une exception soit levée si l id de l album passé en paramètre est invalide. Q3.5 Indiquer que la méthode deletealbum de la classe AlbumResource peut lever des exception de type AlbumNotFoundException et modifier la méthode pour qu elle retourne une javax.ws.rs.core.response de statut OK. public Response deletealbum(@pathparam(value = "id") String albumid) throws AlbumNotFoundException { service.deletealbum(albumid); return Response.ok().build(); Q3.6 Ajouter les tests unitaires permettant de vérifier le code retour attendu lors de la suppression d un album existant et non existant. Vérifier que le message d erreur est bien envoyé par le serveur dans le second cas. @Test public void testdelalbumok() { ClientResponse r = client.del("1"); // assertequals @Test public void testdelalbumnotok() { ClientResponse r = client.del("3"); // assertequals System.out.println(r.getEntity(String.class)); 7