samedi 16 avril 2011

1er anniversaire des Duchess France

C'est ce 14 avril que s'est tenu le 1er anniversaire des Duchess France.
Pour ceux qui n'ont pas suivi, non la noblesse n'a pas été rétablie en France. Il s'agit d'un groupe principalement composé de filles et qui oeuvre pour donner plus de visibilité aux femmes dans le milieu informatique; et bien sûr pas en gestion de projet, mais dans la technique.

A l'arrivée, nous recevons notre badge ainsi qu'un classique petit questionnaire : comment avez vous connu les Duchesses ? Quelle thèmes souhaiteriez voir traité .... A remettre dans une urne, car il pourra faire gagner l'un des lots offerts par les Sponsors. Tout les badges sont marqués d'une petite forme colorée. Quelle est sa signification ? C'est en rentrant dans l'amphithéâtre que la question trouve sa réponse : Nous sommes placés d'après la couleur de gommette. Et c'est ainsi que sont faites les équipes du Trivial Java, ainsi pas de jaloux dans la formation de ces dernières.
La soirée commence par une traditionnelle présentation, ce sont elles/qui elles ne sont pas, quel est le but ?, .... On connaissait déjà les avant-JUG, les groupes de préparation à la certification et bientôt s'annonce un nouvel événement : la marmite !
Mais qu'est ce donc ? Il s'agira d'une rencontre type coding dojo "main dans le code" (ou plutôt dans la marmite).
Après quelques remaniement de certaines couleurs en sous effectif, le jeu peu vraiment commencer. Les règles sont assez simples, chaque équipe envoi un représentant répondre à une série de question sur un thème comme le code java, la veille techno, les frameworks et même l'histoire geeko-javaïste. Des questions bien tordue, notamment sur le code java.
C'est après quelques séries de questions qu'a lieu la pause buffet bien méritée après ces efforts. Celle-çi est l'occasion de discuter avec les diverses connaissances présentes.
Au retour ce celui ci a lieu le tant attendu tirage au sort permettant de gagner les cadeaux offerts par les sponsors (dont Sfeir fait bien sûr parti). Parmi ces lots : un ipad2 (d'ailleurs remporté par une Sferienne : Yasmine Aite), des formations, des pass parleys, .... ainsi qu'un "cadeau surprise" qui se révélât être la fameuse Barbie informaticienne qui avait fait le buzz l'année dernière, remportée par Brice Argenson



La seconde parti reprend sur la lancée de la 1ère partie. Pour ce terminer, sur une égalité entre les verts foncés et les ciels. Pour les départager, c'est un duel entre Julien Dubois et Cédric Beurtheret sur une question de rapidité qui tranchera. C'est Spock (le framework de test) qui emmènera les verts foncés à la victoire par la réponse de Cédric. Leur équipe s'est vue remettre un mug aux couleurs des Duchess.




Pour les plus chevronnés, la soirée se termine par une traditionnelle 3ème mi-temps autour d'une pizza.

Félicitations aux duchesses pour cette belle soirée avec une très bonne animation.
En attendant le prochain anniversaire, nous espérons de bonnes marmites pleines de bonnes technos auxquelles nous voulons tous gouter.

mardi 12 avril 2011

Du Jersey, du Guice et de l'App Engine 3/3

Et pour finir cette série d'article, nous allons nous intéresser à la génération de JSon avec Jersey et bien sûr à sa testabilité.

Le xml, c'est bien gentil, mais dans des échanges REST ça peut être un peu lourd, surtout si le consommateur est un appareil mobile.
La génération JSon avec Jersey peut s'appuyer sur JAX-B. Et oui, c'est justement pour cela que l'on s'en est occupé dans le précédant article. Le mapping, lui, ne change pas.

La resource ne nécessite qu'un petit changement :

@GET
 @Path("{name}")
 @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) 
 public Hello reply(@PathParam("name") String name){
  return helloService.saysHelloToSomeone(name);
 }

Comme on est sympa, on permet de renvoyer soit du Json, soit du Xml, c'est le consommateur qui décide. Par défaut, c'est le 1er format qui est choisit, soit le JSon.

La génération JSon va nécessiter un peu de configuration, principalement à cause du type de JSon généré. C'est le ContextResolver qui va s'occuper de ça :

@Provider
@Singleton
public class JAXBContextResolver implements ContextResolver<JAXBContext> {

 /** Package that contains object that can be mapped */
 private static final String JAXB_OBJECT_PACKAGE = Hello.class.getPackage().getName();

 private final JAXBContext context;

 public JAXBContextResolver() throws Exception {
  this.context = new JSONJAXBContext(JSONConfiguration.natural().build(), JAXB_OBJECT_PACKAGE);
 }

 @Override
 public JAXBContext getContext(Class objectType) {
  if(objectType.getPackage().getName().equals(JAXB_OBJECT_PACKAGE)){
   return context;
         }
  return null;
 }
}

Ici le type de JSon souhaité est le natural.
Cet objet doit être passés dans le même package que les resources, il profitera ainsi lui aussi de la découverte automatique au démarrage de Guice.
Ce resolver n'est pas obligatoire, sans lui, le JSon généré est par défaut en mode mapped.

La configuration des tests, va devoir évoluer un peu pour prendre en compte notre génération en ode natural.
La méthode configure() devient :

@Override
 protected AppDescriptor configure() {
  ClientConfig clientConfig = new DefaultClientConfig();
  clientConfig.getClasses().add(JAXBContextResolver.class);
  injector = Guice.createInjector(new ServletModule() {
   @Override
   protected void configureServlets() {
    bind(getTestingResourceClass());
    bind(JAXBContextResolver.class);
    serve("/*").with(GuiceContainer.class);
   }
  }); 
  return new WebAppDescriptor.Builder()
           .contextListenerClass(GuiceTestConfig.class)
           .filterClass(GuiceFilter.class)
           .clientConfig(clientConfig)
           .servletPath("/")
           .build();
 }

Ainsi du coté serveur comme du coté client, les échanges seront dans le même format. Nos tests deviendront :

@Test
 public void shoulReplyHelloInXml(){
  doShoulReplyHello(MediaType.APPLICATION_XML_TYPE);
 }
 
 @Test
 public void shoulReplyHelloInJson(){
  doShoulReplyHello(MediaType.APPLICATION_JSON_TYPE);
 } 
 
 private void doShoulReplyHello(MediaType type){
  String message = "Hello";
  String name ="Nicolas";
  Hello hello = new Hello(message, name);
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(hello);
  
  ClientResponse response = resource().path("hello").path(name).accept(type).get(ClientResponse.class);
  
  verify(helloServiceMock).saysHelloToSomeone(name);
  assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
  assertThat(response.getType()).isEqualTo(type);
  Hello entity = response.getEntity(Hello.class);
  assertThat(entity).isNotNull().isEqualTo(hello);  
  
 } 

Une des différenciation entre les types de JSon générés se fait sur la façon dont sont écrites les listes. En mode natural, nous avons par exemple : [objet1, objet2, ...] avec des objet {"attributA":"valeurA", ....}

Imaginons que nous avons une autre resource qui par grande politesse retourne 2 Hellos :

@Path("doublehello")
@Singleton
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class DoubleHelloResource {
 
 @Inject
 private HelloService helloService;
 
 @GET
 @Path("/{name}")
 public List<Hello> reply(@PathParam("name") String name){
  List<Hello> hellos = new ArrayList<Hello>();
  hellos.add(helloService.saysHelloToSomeone(name));
  hellos.add(helloService.saysHelloToSomeone(name));
  return hellos;
 }
 
}

Pour vérifier sa bonne génération, nous aurions le test suivant :

@Test
 public void shoudHaveTwoHello(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(new Hello(message, name)); 
  ClientResponse response = resource().path("doublehello").path(name).get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
  List hellos = response.getEntity(new GenericType<List<Hello>>(){});
  assertThat(hellos).isNotNull().hasSize(2);
 }
 
 @Test
 public void shoudBeInNaturalJson(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(new Hello(message, name)); 
  ClientResponse response = resource().path("doublehello").path(name).get(ClientResponse.class);
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType()).isEqualTo(MediaType.APPLICATION_JSON_TYPE);
  String hellos = response.getEntity(String.class);
  assertThat(hellos).isEqualTo(naturalHelloJSon(message, name));
 }
 
 public String naturalHelloJSon(String message, String name){
  StringBuilder sb = new StringBuilder();
  sb.append("[{\"message\":\"").append(message).append("\",\"name\":\"").append(name).append("\"},");
  sb.append("{\"message\":\"").append(message).append("\",\"name\":\"").append(name).append("\"}]");
  return sb.toString();
 }

Même s'il est un format intéressant, le JSon souffre d'un problème lié au javascript : celui du cross-domain qui fait que l'on ne peut pas interroger un autre domain que celui de la page web.
JSonP permet d'évincer cette contrainte.

Jersey permet aussi de générer ce type de réponse mais un peu moins facilement.
Nous allons créer une nouvelle méthode pour ce type de réponse :

@GET
 @Path("{name}.jsonp")
        @Produces("application/x-javascript")
 public JSONWithPadding replyWithJsonP(@PathParam("name") String name, @QueryParam("callback") @DefaultValue(CALLBACK_DEFAULT_NAME) String callback){
  Hello hello = helloService.saysHelloToSomeone(name);
  return new JSONWithPadding(hello, callback);
 } 

Son test reste dans l'optique des précédents :

@Test
 public void shoudBeJsonpWithCallbackNameParam(){
  String message = "Hello";
  String name ="Nicolas";
  when(helloServiceMock.saysHelloToSomeone("Nicolas")).thenReturn(new Hello(message, name));
  String callbackName = "monCallback";
  
  ClientResponse response = resource().path("hello").path(name+".jsonp").queryParam("callback", callbackName).get(ClientResponse.class);
  
  assertThat(response.getStatus()).isEqualTo(Status.OK.getStatusCode());
  assertThat(response.getType().toString()).isEqualTo("application/x-javascript");
  assertThat(response.getEntity(String.class)).isNotNull().startsWith(callbackName);
 }

Je n'ai pas malheureusement pas trouvé comment unmarshmaller ce message.


Et voilà, ce tour d'horizon est fini, amusez vous bien avec ces quelques technos.
Comme à chaque fois, le code source est disponible.

mardi 5 avril 2011

Intégration du SDK Facebook dans une application Android

Cet article est une version écrite de la présentation que j'ai pu faire au BOF dernier de Sfeir.

Pourquoi ?

Faire connaitre son application est une problématique courante auquel est confronté le développeur Android. Donner un aspect "social" à son application peut être une solution.
Facebook est aujourd'hui une référence absolue en matière de réseau social et peut donc contribuer à répondre à cette problématique grace aux services qu'ils expose pour les développeurs. Voici quelles fonctionnalités qui pourrait donner la dimension sociale voulue :
  • Publier sur son mur : exprimer son avis.
  • Organisation d'évènements et inviter des amis à y participer
  • Checkins : marquer sa position

Néanmoins l'intégration de Facebook dans une application Android peut susciter une certaine crainte de la part des utilisateurs :
  • Si je dois m'authentifier au travers de l'application, n'y a t-il pas un risque qu'on me vole mes identifiants ? De plus, le fait de devoir me rentrer mes identifiants va certainement freiner l'envie de l'utilisateur de s'exprimer
  • Cette application ne risque t-elle pas de d'acceder à mes informations pour les renvendre et ma boite mail va être innondées de spam ?

Quoi ?

Le SDK Facebook constitue une solution pour le développeur Android.
En s'appuyant sur l'application officielle, la connexion est ainsi quasiment invisible. Celle çi fait seulement valider à l'application les droits dont dispose l'application sur le compte de l'utilisateur.

Ce SDK n'est qu'un simple adaptateur entre du code en java et la Graph API. Né dans l'esprit de Mark Zuckerberg, elle permet d'accéder et d'interagir avec les données Facebook. En terme plus courant, c'est simplement un ensemble de services REST produisant du JSon.

Imaginons que je souhaite publier sur mon mur, la documentation nous donne :




La requête à adresser sera : me/feed
Et nécessitera l'autorisation de publication de flux.

Comment ?

Tout d'abord, il faut télécharger le SDK. Celui fonctionnant sur le principe d'une library android, il suffit d'ajouter la dépendance nécessaire. Le SDk est fourni avec quelques exemples de manipulation de l'api.
Il faudra aussi enregistrer son application sur Facebook. Déclarez vous en tant que développeur en donnant votre numéro de téléphone si cela n'est pas déjà le cas. La déclaration de l'application permettra permettra d'obtenir un identifiant d'application. Celui ci sera utilisé dans le code Android. Et pour finir la dernière étape : la création de la clé d'après le certificat qui signe l'apk :

keytool -exportcert -alias monApplication -keystore ~/.android/monApplication.keystore | openssl sha1 -binary | openssl base64

Pour les utilisateurs de Windows, il est recommande de passer par un outil comme cygwin pour éviter des problèmes de génération avec openssl.

Sans cette clé configuré, il sera impossible à l'application d'accéder aux services Facebook. L'avantage est que même si le code Android est décompilé, il sera impossible d'utiliser le compte de l'application. L'inconvénient est que pour tester le bon fonctionnement, il faudra générer un apk signé à chaque fois que ce soit pour un téléphone, ou sur l'émulateur.

Il ne reste plus qu'à coder

Si votre application ne possède pas le droit d'accès à internet, n'oublier pas de le rajouter dans le fichier AndroidManifest



La majeure partie du SDK est rassemblée dans l'objet Facebook
Pour instancier cet objet, il suffit de lui passer en paramètre l'identifiant de l'application.

private static final Facebook mFacebook = new Facebook(FACEBOOK_APP_ID);

Un aspect primordial dans une application Android est la gestion asynchrone des tâches dès lors que l'on execute.

private static final AsyncFacebookRunner mAsyncFacebookRunner = new AsyncFacebookRunner(mFacebook);

Lors sa tentative de connexion, le SDK envoie un intent à l'application officielle Facebook. Il est donc important de spécifier le code retour que devra avoir de cette activité, surtout si la votre utilise aussi ce mécanisme.

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
     if(requestCode == FACEBOOK_REQUEST_CODE){// Retour de login facebook
      FacebookFunctions.handleLoginResult(resultCode, data);      
     }
    }

C'est lors de la demande de connexion que sera passé ce code retour. Cette demande doit aussi spécifier quelles sont les permissions requises par l'application.

private static final String PUBLISH_PERMISSION = "publish_stream";
private static final String[] PERMISSIONS = new String[] { PUBLISH_PERMISSION };
mFacebook.authorize(activity, PERMISSIONS, facebookRequestCode, new LoginDialogListener());

Quant à l'action de publication sur le mur, c'est une simple wrapping de requête web :

public static void publishCommentOnWall(String comment, PocRequestListener requestListener){
  final Bundle parameters = new Bundle();
  parameters.putString(GP_LINK_PARAM_FEED, ANDROID_URL);
  parameters.putString(GP_NAME_PARAM_FEED, "Android");
  parameters.putString(GP_PICTURE_PARAM_FEED, ANDROID_IMAGE_URL); 
  parameters.putString(GP_DESCRIPTION_PARAM_FEED, comment);  
  mAsyncFacebookRunner.request(GP_ME_FEED_URI, parameters, GP_POST_REQUEST, requestListener, null); 
 }

Les paramètres de la méthode request sont :
- La requête à effectuée, soit notre me/feed
- Les paramètre de cette requête.
- Le type de requête, soit du POST
- Un callback de réponse.
- Un objet quelconque de synchronisation qui sert à identifier les appels lorsqu'on en fait plusieurs en même temps. Il est facultatif.


J'ai moi même réalisé l'intégration de Facebook au sein de Keoli TV, une application Android permettant de donner le programme TV en temps réel, projet personnel sur lequel je travail avec quelques amis.

dimanche 3 avril 2011

Du Jersey, du Guice et de l'App Engine 2/3

Dans le dernier article, nous disposions d'un service Hello World qui renvoyait du texte brut.
Cette fois, nous allons lui ajouter la capacité à répondre du xml en sérialisant le message avec JAX-B.

Tout d'abord, notre nouvelle réponse sera faite par l'objet suivant :

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Hello {
 
 private String message;
 private String name;
 
 public Hello(){
 }
 
 public Hello(String message, String name) {
  super();
  this.message = message;
  this.name = name;
 }
 
 public String getMessage() {
  return message;
 }
 
 public void setMessage(String message) {
  this.message = message;
 }
 
 public String getName() {
  return name;
 }
 
 public void setName(String name) {
  this.name = name;
 }
 
 @Override
 public int hashCode() {
  return Objects.hashCode(message, name);
 }
 
 @Override
 public boolean equals(Object obj) {
     if(obj instanceof Hello){
         final Hello other = (Hello) obj;
         return Objects.equal(message, other.message)
             && Objects.equal(name, other.name);
     } else{
         return false;
     }
 }
}

La ressource exposée évolue peu :

@Path("hello")
@Singleton
@Produces(MediaType.APPLICATION_XML)
public class HelloResource {
 
 @Context 
 UriInfo uriInfo; 
 
 @Inject
 private HelloService helloService;
 
 @GET
 @Path("/{name}")
 public Hello reply(@PathParam("name") String name){
  return helloService.saysHelloToSomeone(name);
 }

 @POST
 public Response send(String name){
  Hello hello = helloService.sendHello(name);
  URI uri = uriInfo.getAbsolutePathBuilder().build();
  return Response.created(uri).entity(hello).build();
 }  
 
 
 public void setHelloService(HelloService helloService) {
  this.helloService = helloService;
 }
 
}

Les opérations de marshall/unmarshall sont opérées directement par Jersey lui même.


De même, les tests vont peu évoluer, seul le type de données attendu va changer. Nous aurons par exemple :

@Test
 public void shoulReplyHello(){
  String name ="Nicolas";
  String hello = "Hello "+name;
  when(helloServiceMock.saysHelloToSomeone(name)).thenReturn(hello);
  
  ClientResponse response = resource().path("hello").path(name).get(ClientResponse.class);
  
  verify(helloServiceMock).saysHelloToSomeone(name);
  assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
  assertThat(response.getType()).isEqualTo(MediaType.TEXT_PLAIN_TYPE);
  assertThat(response.getEntity(String.class)).isEqualTo("Hello Nicolas");
  
 }

Et c'est tout, pour aujourd'hui ...

Bon ok, je reconnais, c'est un peu l'arnaque cet article, il y a peu de choses à faire. Mais n'est ce pas justement ça qui est intéressant, non ?

La prochaine fois, nous terminerons cette ballade autour de Jersey en générant du JSon.

Le code est disponible ici.

samedi 2 avril 2011

Git & Geeks

Tout commence par quelques tweets qui fusent, pour un besoin projet, quelques Sferiens demandent à 2 autres Sferiens Git lovers, une petite présentation pour les aider à appréhender l'outil.
C'est par quelques autres tweets que le cercle des personnes intéressées s'élargit et finit par donner lieu à 2 ateliers Git d'une heure et demi organisés en fin de journées.
Bien que ces 2 soirées de présentation débutant n'ont pas eu les même présentateurs, elles étaient complementaires.
Lors de la 1ère, @fsznajderman nous a exposé les commandes de base et nous nous sommes amusés à cloner un simple répertoire contenant un fichier qu'il avait mis à notre disposition sous Assembla. Cela, nous à permis de nous familiariser avec quelques commande de base, mais aussi à voir comment gérer des conflits. Et oui, si on s'amuse à modifier la même ligne, il est tout de même perdu...
Pour la 2ème, @ptit_fred nous a plongé dans la dimension du graphes des commits de Git et nous a appris à mettre un projet sous git, de manier les branches et de les merger.
Personnellement, ces 2 séances m'ont donné l'envie d'étendre mes connaissances et d'après l'avis des participant(e)s, je ne suis pas le seul.
Devant cet enthousiasme, une suite devrait avoir lieu.
Sfeir, c'est ça aussi, une bande de geeks qui aime échanger sur divers sujets.

mercredi 30 mars 2011

Du Jersey, du Guice et de l'App Engine 1/3

Jersey et Guice offre son un assemblage intéressant pour exposer un service REST sur App Engine. Mais comment puis je tester convenablement de cocktail ?
Dans son api, Jersey propose la classe JerseyTest qui permet de démarrer un server en mémoire ainsi que quelques méthodes d'aide pour construire ses requêtes.
En réalité, le fait d'être sur AppEngine, ne va rien changer à ce que nous en ferons au sein de cet article.

Un peu de code

Nous allons utiliser une resource toute simple : Hello.
Pour cet article, nous allons commencer par utiliser un type de retour simple lui aussi : du texte brut. Nous disposerons de 2 méthodes, un GET et un POST.

Voici notre ressource :

@Path("hello")
@Singleton
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.TEXT_PLAIN)
public class HelloResource {
 
 @Context 
 UriInfo uriInfo;
 
 @Inject
 private HelloService helloService;
 
 @GET
 @Path("/{name}")
 public String reply(@PathParam("name") String name){
  return helloService.saysHelloToSomeone(name);
 }
 

 @POST
 public Response send(String name){
  String hello = helloService.sendHello(name);
  URI uri = uriInfo.getAbsolutePathBuilder().build();
  return Response.created(uri).entity(hello).build();
 } 
 
 
 public void setHelloService(HelloService helloService) {
  this.helloService = helloService;
 }
 
}

Je ne vais pas rentrer dans les détails de l’implémentation, ce n'est pas le but de cet article.
Le HelloService utilisé est le suivant :

@Singleton
public class HelloService {
 
 public String saysHelloToSomeone(String name){
  return "Hello "+name;
 }

 public String sendHello(String name) {
  return "Hello "+name;
 }

}

Et pour finir, nous aurons la configuration Guice suivante pour fonctionner correctement avec Jersey :

public class GuiceServletConfig extends GuiceServletContextListener {
 
 private static final String SDAAS_SERVER_RESOURCES_PACKAGE = HelloResource.class.getPackage().getName();
 private static final String JERSEY_CONFIG_PROPERTY_PACKAGES = "com.sun.jersey.config.property.packages"; 

 @Override
 protected Injector getInjector() {
  final Map params = new HashMap<String, String>();
  params.put(JERSEY_CONFIG_PROPERTY_PACKAGES, SDAAS_SERVER_RESOURCES_PACKAGE);
  
  return Guice.createInjector(new ServletModule() {

   @Override
   protected void configureServlets() {
    serve("/*").with(GuiceContainer.class, params);
   }
  });
 }
}

Les ressources exposées sont découvertes automatiquement par l'utilisation du paramètre de clé JERSEY_CONFIG_PROPERTY_PACKAGES. Il leur suffit donc que mes ressources soit dans le package pointé par la valeur de ce paramètre pour qu'elle soit prise en compte.

Et un zeste de test pour garder la fraicheur

Il nous reste plus qu'à mettre en place quelques tests dessus.
Il v falloir créer une classe de test qui hérite de JerseyTest.
Comme je disais, celui ci va lancer son serveur en mémoire, il nécessite donc une dépendance supplémentaire :

<dependency>
   <groupid>com.sun.jersey.jersey-test-framework</groupid>
   <artifactid>jersey-test-framework-grizzly</artifactid>
   <version>${jersey.version}</version>
   <scope>test</scope>
</dependency> 

Fonctionnant par un mécanisme spi, il n'aura pas besoin de configuration supplémentaire pour se lancer.
JerseyTest n'utilise pas le web.xml et doit donc être configuré par du code; soit par le constructeur, soit par l'implémentation de la méthode protected AppDescriptor configure(). Nous allons privilégier le second choix. D'autant plus que nous allons devoir utiliser un autre Injector Guice pour pouvoir mocker HelloResource, sans quoi nous ne pourrons pas récupérer l'instance de HelloResource. Un autre avantage est d'isoler la ressource que je souhaite tester.

private static Injector injector; 
private HelloService helloServiceMock;

@Before
public void setUp() throws Exception {
 super.setUp();
 HelloResource helloResource =  injector.getInstance(HelloResource.class);
 helloServiceMock = mock(HelloService.class);
 helloResource.setHelloService(helloServiceMock);
} 

@Override
protected AppDescriptor configure() {
 injector = Guice.createInjector(new ServletModule() {
  @Override
  protected void configureServlets() {
   bind(HelloResource.class);
   serve("/*").with(GuiceContainer.class);
  }
 });  
 return new WebAppDescriptor.Builder()
              .contextListenerClass(GuiceTestConfig.class)
              .filterClass(GuiceFilter.class)
              .servletPath("/")
              .build();
}

private static class GuiceTestConfig extends GuiceServletContextListener {
 @Override
 public Injector getInjector() {
  return injector;
 }
}


Il n'y a plus qu'à se créer des méthodes de tests

@Test
public void shoulReplyHello(){
 String name ="Nicolas";
 String hello = "Hello "+name;
 when(helloServiceMock.saysHelloToSomeone(name)).thenReturn(hello);
 
 ClientResponse response = resource().path("hello").path(name).get(ClientResponse.class);
 
 verify(helloServiceMock).saysHelloToSomeone(name);
 assertThat(response.getClientResponseStatus()).isEqualTo(Status.OK);
 assertThat(response.getType()).isEqualTo(MediaType.TEXT_PLAIN_TYPE);
 assertThat(response.getEntity(String.class)).isEqualTo("Hello Nicolas");
 
}

@Test
public void shouldSendHello(){
 String name ="Nicolas";
 String hello = "Hello "+name;
 when(helloServiceMock.sendHello(name)).thenReturn(hello);

 ClientResponse response = resource().path("hello").post(ClientResponse.class,name);

 verify(helloServiceMock).sendHello(name);
 assertThat(response.getClientResponseStatus()).isEqualTo(Status.CREATED);
 assertThat(response.getType()).isEqualTo(MediaType.TEXT_PLAIN_TYPE);
 assertThat(response.getEntity(String.class)).isEqualTo("Hello Nicolas");
}

Je vérifie que lorsque j'appelle la bonne url, mon résultat correspond à ce que je veux, soit un Hello qui m'est adressé, au bon format et le bon code retour.
Dans le prochain épisode, nous ajouterons une sérialisation JAX-B.



Le code est disponible ici.

Références :
http://blog.iparissa.com/google-app-engine-jax-rs-jersey/

lundi 14 mars 2011

Etendre les assertions de Fest Assert

C'est lors de la présentation de David Gageot sur les test au Paris JUG du mois de janvier que j'ai découvert Fest-Assert. J'ai rapidement été enthousiaste sur son utilisation. En plus de sa syntaxe proche d'un langage naturel, il permet d'étendre ses assertions en fonction de ses besoins.
Voici 2 façons d'utiliser ce mécanisme :

Utilisation d'une Condition.

Ce cas de figure est à utiliser lorsque l'objet que je souhaite vérifier possède déjà son objet d'assertion (StringAssert, FileAsset, ... ) ou si celle ci est simple.
Il faut pour cela étendre la classe Condition<T> et implémenter  la méthode public boolean matches(<T> value).
Imaginons pas exemple que je souhaite vérifier que ma liste contienne un nombre impair d'éléments. Je vais écrire la conditions suivantes :

private class IsListeTailleImpaireCondition extends Condition<List<?>> { 
 
     @Override 
     public boolean matches(List value) { 
          if(value == null){ 
               return false; 
          } else { 
               return value.size()%2==1; 
          } 
     } 
 
} 
Son utilisation s'avère très simplement :
assertThat(maListe).is(new IsListeTailleImpaireCondition()); 
ou :
assertThat(maListe).satifies(new IsListeTailleImpaireCondition()); 

La différente entre les 2, n'est qu'au niveau sémantique, la validation est équivalente.
Et si on veut vérifier le contraire alors ? Rassurez vous, pas besoin d'écrire une condition inverse, Fest-Assert possède des méthodes pour ça : .isNot(...) ou .doesNotSatisfy(...)


Créer son objet d'assertion.

Ce cas de figure est adapté aux cas où mon objet ne possède pas son objet d'assertions. Là, depuis la version 1.4 qui date d'il y a peu, c'est devenu beaucoup plus simple. : il suffit d'étendre la classe GenericAssert et d'implémenter les méthodes que je souhaite, sans oublier bien sûr de retourner une instance de l'objet d'assertion afin de pouvoir chaîner les méthodes.
Imaginons que je souhaite développer mes assertions sur DateTime de jodatime :
import static org.fest.assertions.Formatting.inBrackets; 
import static org.fest.util.Strings.concat; 
 
import org.fest.assertions.GenericAssert; 
import org.joda.time.DateTime; 
 
 
/**
 * Assertions for <code>{@link org.joda.time.DateTime}</code>.
 */ 
public class DateTimeAssert extends GenericAssert<DateTimeAssert, DateTime>  { 
       /**
         * Creates a new {@link org.joda.time.DateTimeAssert}.
         * @param actual the target to verify.
         */  
 public DateTimeAssert(DateTime actual) { 
     super(DateTimeAssert.class, actual); 
 } 
 
 /**
  * Vérifie que le {@code org.joda.time.DateTimeAssert} est avant celui passé en paramètre.
  * @param Le {@code org.joda.time.DateTimeAssert} avec lequel on compare.
  * @return L'objet d'assertion.
  */ 
 public DateTimeAssert isBefore(DateTime expected){ 
     isNotNull();   
     if(actual.isBefore(expected)) { 
      return this; 
     } 
     failIfCustomMessageIsSet(); 
     throw failure(concat(actual(), " should be before to :", inBrackets(expected))); 
 } 
  
 /**
  * Vérifie que le {@code org.joda.time.DateTimeAssert} est avant ou égale à celui passé en paramètre.
  * @param Le {@code org.joda.time.DateTimeAssert} avec lequel on compare.
  * @return L'objet d'assertion.
  */ 
 public DateTimeAssert isBeforeOrEquals(DateTime expected){ 
     isNotNull();   
     if(actual.compareTo(expected) <= 0) { 
      return this; 
     } 
     failIfCustomMessageIsSet(); 
     throw failure(concat(actual(), " should be before to :", inBrackets(expected))); 
 }  
  
 /**
  * Vérifie que le {@code org.joda.time.DateTimeAssert} est après celui passé en paramètre.
  * @param Le {@code org.joda.time.DateTimeAssert} avec lequel on compare.
  * @return L'objet d'assertion.
  */  
 public DateTimeAssert isAfter(DateTime expected){ 
     isNotNull();   
     if(actual.isAfter(expected)) { 
      return this; 
     } 
     failIfCustomMessageIsSet(); 
     throw failure(concat(actual(), " should be after to :", inBrackets(expected))); 
 } 
  
 /**
  * Vérifie que le {@code org.joda.time.DateTimeAssert} est après ou égale à celui passé en paramètre.
  * @param Le {@code org.joda.time.DateTimeAssert} avec lequel on compare.
  * @return L'objet d'assertion.
  */  
 public DateTimeAssert isAfterOrEquals(DateTime expected){ 
     isNotNull();   
     if(actual.compareTo(expected) >= 0) { 
      return this; 
     } 
     failIfCustomMessageIsSet(); 
     throw failure(concat(actual(), " should be after to :", inBrackets(expected))); 
 }  
  
 /**
  * Vérifie que le {@code org.joda.time.DateTimeAssert} est compris dans l'intervale de ceux passés en paramètre.
  * @param Le {@code org.joda.time.DateTimeAssert} de début d'intervallle
  * @param Le {@code org.joda.time.DateTimeAssert} de fin d'intervallle
  * @return L'objet d'assertion.
  */ 
 public DateTimeAssert isBetween(DateTime begin, DateTime end){ 
     isNotNull(); 
     if(actual.compareTo(begin) >= 0 && actual.compareTo(end) <= 0) { 
         return this; 
     } 
     throw failure(concat(actual(), " should be between :", inBrackets(begin), " and ", inBrackets(end)));   
 } 
  
 
 private String actual() { 
     return inBrackets(actual); 
 } 
  
} 
Pour m'en servir, je dois le rattacher à un point d'entrée, comme Assertions classe de base de Fest-Assert.
public class MyAssertions { 
 
 public static DateTimeAssert assertThat(DateTime actual) { 
     return new DateTimeAssert(actual); 
 } 
  
} 
Et pour finir, je n'aurais plus qu'à faire par exemple :
MyAssertions.assertThat(maDate).isBeetwen(debutPeriode,finPeriode);