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);