JUnit Test the REST!


Ormai sviluppare servizi web secondo l’architettura REST sembra una moda. Era quello che in fondo pensavo prima di leggere REST in Practice, un libro da leggere assolutamente per capire come approcciarsi a questo nuovo modo di accesso a risorse web. Che poi tanto nuovo non è… bastava accorgersi che HTTP aveva già tutto quello che occorreva per essere un “Application Protocol” e non semplicemente un “Transport Protocol” come siamo abituati ad usarlo. Siamo abituati infatti a vedere solamente i metodi GET e POST, perché i browser supportano solo quelli, e che spesso usiamo indistintamente senza capirne la vera differenza. Se volgiamo quindi testare un servizio che usa altri metodi come PUT o DELETE (necessari per realizzare un servizio CRUD) come facciamo?

Basta un poco di test e la pillola va giù…

Quando realizziamo un servizio, come facciamo a sapere se funziona? Ma con JUnit!! E chi non lo voleva usare, o ne era allergico, questa volta non avrà scampo! Nemmeno l’amato browser potrà salvarlo! Ovviamente è possibile scegliere innumerevoli soluzioni da integrare con JUnit per rendere automatici i test. Di seguito, quelli con cui ho “giocato” prima di decidere quale scegliere. Ammettiamo di voler testare un servizio CRUD così fatto:

@Path("/order")
public class OrderService {

   @POST
   @Path("/new")
   public Response create(Order order) {
      ...
   }
   
   @PUT
   @Path("/{orderId}")
   public void update(Order order) {
     ...
   }
   
   @GET
   @Path("/{orderId}")
   public Order retrieve(@PathParam("orderId") String id) {
      ...
   }
   
   @DELETE
   @Path("/{orderId}")
   public void delete(@PathParam("orderId") String id) {
      ...
   }  
}

con il quale è possibile creare, aggiornare, modificare o cancellare un ordine.

RESTEasy Server-side mock Framework

Se lavorate con JBoss AS, o direttamente con RESTEasy, avete a disposizione un framework per simulare richieste HTTP ad un server fittizio.

@Test
public void testRetrieve() throws Exception {
   Dispatcher dispatcher = MockDispatcherFactory.createDispatcher();
   POJOResourceFactory service = new POJOResourceFactory(OrderService.class);
   dispatcher.getRegistry().addResourceFactory(service);
   MockHttpRequest request = MockHttpRequest.get("/order/12");
   MockHttpResponse response = new MockHttpResponse();

   dispatcher.invoke(request, response);
   assertEquals(HttpServletResponse.SC_OK, response.getStatus());
}

E’ molto semplice generare una chiamata al servizio, soprattutto per chi ha familiarità con le Servlet. Con questo approccio Servlet-like, si ha il controllo totale del protocollo HTTP, a scapito di qualche scorciatoia che potrebbe farci comodo per il marshalling/unmarshalling del payload delle richieste o delle rispose. Possiamo infatti leggere il contenuto della risposta come stringa, array di byte o stream (non proprio comodo)!
Comodo però è il fatto che gestione mock delle richieste è integrata in RESTEasy, per cui fortunatamente non si necessita di codice di terze parti.
Il grande vantaggio di questa soluzione è che si riescono a creare test di unità senza la necessità di avere il server avviato… almeno finché l’implementazione dei metodi lo permette! Per esempio se il nostro servizio è arricchito da Interceptors o Decorators CDI (ebbene si signori, funzionano!! Weld e RESTEasy a stanno molto bene insieme… almeno in JBoss 7 😉 ), questi non vengono chiamati, per non parlare di contesti transazionali. In effetti, non abbiamo nemmeno avviato il server! I puristi dei test potrebbero obiettare che se ho bisogno dei servizi del server non sto facendo test di unità ma magari di integrazione, ma qui si apre un mondo che è meglio non affrontare…
Quindi se avete decorato il servizio, o avete bisogno di servizi dell’Application Server, è necessario trovare una strada alternativa.

RESTEasy Client API

Anche se la specifica JAX-RS (cioè REST per Java EE 6) al momento non prevede una standardizzazione del lato consumer dei servizi REST, i vari vendor si sono arrangiati a proporre soluzioni proprietarie. Possiamo quindi usare per esempio la client API di RESTEasy per testare i servizi (magari ci fa proprio comodo se anche il server è stato realizzato con questa implementazione della specifica…). Possiamo quindi scrivere un test di unità come segue:

private static final String BASE_URI = "http://localhost:8080/JBoss71TestWeb/rest/order";
   
@Test
public void testRetrieve() throws Exception {
   OrderServiceInterface orderService = ProxyFactory.create(OrderServiceInterface.class, BASE_URI);
   Order order = orderService.retrieve("12");
   
   assertEquals("12", order.getId());
}

Con due righe di codice riusciamo a testare il servizio, Interceptors/Decorators compresi! Questa volta però abbiamo dovuto avviare il server a differenza di prima! L’approccio in questo caso è molto di alto livello: per creare un proxy del servizio è stata creata una interfaccia con gli stessi metodi e le stesse annotazioni che espone il lato server. Sarà cura di RESTEasy costruire le chiamate HTTP dall’interfaccia tramite Apache HttpClient, del tutto trasparente all’utente. Come si vede però, si perde il pieno controllo del protocollo: non è possibile per esempio specificare nessun parametro sulla richiesta perché totalmente nascosta dal proxy.

RESTFuse

RESTFuse è un progetto molto interessante: si presenta come estensione a JUnit per testare richieste HTTP. Di seguito un esempio:

@RunWith( HttpJUnitRunner.class )
public class RestfuseTest {

  @Rule
  public Destination destination = new Destination( "http://localhost:8080/MyRestApp/api" ); 

  @Context
  private Response response;

  @HttpTest(method = Method.GET, path = "/order/12", headers=@Header(name="accept", value = MediaType.APPLICATION_XML))
  public void testRetrieve() {
    assertOk(response);
    Order order = response.getBody(Order);
    assertEquals("12", Order.getId());
  }  
}

basta quindi specificare:

  • una Destination annotata con @Rule.
  • una Response annotata con @Context. Per ogni metodo di test, verrà iniettata la risposta corrente.
  • un test annotato con @HttpTest nel quale specificare il tipo di metodo, il path e così via.
  • annotare la classe di test con il runner di RESTFuse:
    @RunWith(HttpJUnitRunner.class).

Questo modo dichiarativo di testare i servizi è molto interessante, e ci dà pieno supporto al marshalling/unmarshalling del payload della risposta senza tanto stress. Balza però all’occhio subito una cosa: non si ha nessun controllo sui parametri e sul contenuto della request. Come fare quindi a passare dati, per esempio, per un metodo PUT? L’annotazione @HttpTest è ricca di attributi, dove è possibile specificare:

  • content-type
  • autenticazione
  • headers
  • contenuto sotto forma di stringa o file

Purtroppo quest’ultimo punto lascia un po’ a desiderare. Non è molto comodo scrivere un intero oggetto, in JSON o XML che sia, nell’annotazione o in un file esterno. Se volessi invece usare JAXB, per esempio, o un altro serializzatore non potrei…

Jersey Client API

Se guardiamo tra le dipendenze di RESTFuse, scopriamo che abbiamo tra le mani Jersey! E chi meglio della Reference Implementation di JAX-RS ci può aiutare? Come abbiamo già detto, la parte client non è coperta dalla specifica, ma quella che si sono inventati gli sviluppatori di Jersey è veramente facile da usare e chiara, permettendo allo stesso tempo di avere pieno controllo su request e response.

...
private static final String BASE_URI = "http://localhost:8080/JBoss71TestWeb/rest/order";

@Test
public void testRetrieve() throws ParseException {
   WebResource resource  = Client.create().resource(BASE_URI + "/15");
   Order order = resource.accept(MediaType.APPLICATION_XML_TYPE).get(Order.class);
   assertEquals("15", order.getId());
}

@Test
public void testCreate() throws Exception {
   WebResource resource  = Client.create().resource(BASE_URI + "/new");
   Token token = new Token("11", new Date());
   ClientResponse clientResponse = resource.accept(MediaType.APPLICATION_XML_TYPE).
entity(token).post(ClientResponse.class);
   assertEquals(Response.Status.CREATED.getStatusCode(), clientResponse.getStatus());
   assertEquals(BASE_URI + "/11", clientResponse.getLocation().toString());
}

Con poche righe di codice che parlano da se, il test del nostro servizio è fatto! Il JAXB viene usato automaticamente nel marshalling e unmarshalling della conversazione e ovviamente, se avevamo Interceptors o Decorators come accennato prima, questi vengono tranquillamente chiamati! Guardando i due test, si nota che è possibile recuperare la risposta tramite l’oggetto ClientResponse, oppure direttamente con il nostro oggetto Order, che verrà opportunamente popolato.

Jersey dispone anche di un nuovo progetto che mira l’integrazione con JUnit: vista la semplicità con cui è possibile scrivere un client ne vale davvero la pena?

HttpClient

Se proprio ci vogliamo male, possiamo ricorrere anche ad un livello “più basso” e usare HttpClient di Apache Commons, ma onestamente non ne vedo il motivo vista la mole di codice da scrivere in più e l’assenza di gestione automatica del marshalling/unmarshalling.

...
private static final String BASE_URI = "http://localhost:8080/JBoss71TestWeb/rest/order";

@Test
public void testRetrieve() throws Exception {
   HttpClient client = new DefaultHttpClient();
   HttpGet get = new HttpGet(BASE_URI + "/12");
   get.addHeader("accept", MediaType.APPLICATION_XML);
   HttpResponse response = client.execute(get);
   HttpEntity entity = response.getEntity();
   InputStream stream = entity.getContent();
   try {
      // Read stream
   } finally {
      stream.close();
   }
}

Conclusioni

Che dire, questo è stato il mio excursus nel mondo dei test dei servizi REST. Tra le varie scelte, mi sento di consigliare RESTFuse per un semplice motivo: per le richieste semplici, è estremamente immediato scrivere un test; per quelle più complesse, possiamo ripiegare tranquillamente su Jersey Client che è già integrato! E voi? cosa usate per testare i vostri servizi?

Andrea Como

Sono un software engineer focalizzato nella progettazione e sviluppo di applicazioni web in Java. Presso OmniaGroup ricopro il ruolo di Tech Leader sulle tecnologie legate alla piattaforma Java EE 5 (come WebSphere 7.0, EJB3, JPA 1 (EclipseLink), JSF 1.2 (Mojarra) e RichFaces 3) e Java EE 6 con JBoss AS 7, in particolare di CDI, JAX-RS, nonché di EJB 3.1, JPA2, JSF2 e RichFaces 4. Al momento mi occupo di ECM, in particolar modo sulla customizzazione di Alfresco 4 e sulla sua installazione con tecnologie da devops come Vagrant e Chef. In passato ho lavorato con la piattaforma alternativa alla enterprise per lo sviluppo web: Java SE 6, Tomcat 6, Hibernate 3 e Spring 2.5. Nei ritagli di tempo sviluppo siti web in PHP e ASP. Per maggiori informazioni consulta il mio curriculum pubblico. Follow me on Twitter - LinkedIn profile - Google+

  • Ciao, anch’io in questo momento sto sviluppando servizi Rest, ma sto usando Spring. Per i test sto usando JUnit, ma momentaneamente senza chiamate http, richiamo i metodi dei servizi da codice (Classe.metodo). Mi piacerebbe usare appena ho tempo di farlo, un sistema tipo chiamate ad un server fittizio, come hai fatto nel primo esempio. Preferirei evitare di aver avviato il server per fare i test.

    • Ciao, di Spring mi piace molto il fatto che riesce a tirare su il contesto con una semplice annotazione su una classe di test. Lavorando con EJB la cosa non è così immediata, ma comunque fattibile.
      Per i test di integrazione però non penso sia male avviare il server, almeno hai un test in un ambiente di runtime reale e non “simulato”

      • Dalla versione Spring 3.2 e’ stato introdotto spring-test-mvc,
        http://static.springsource.org/spring-framework/docs/3.2.0.BUILD-SNAPSHOT/reference/htmlsingle/#spring-mvc-test-framework

        sto usando questo per testare le mie chiamate Rest e non, e’ molto comodo perche’ non necessita nessun server esterno e dispone di una interfaccia fluent sia per per i mock che per gli asserts:

        final MockMvc mock = standaloneSetup(homeViewController)
        .setSingleView(new MappingJacksonJsonView()).build();

        mock.perform(get(“/home”))
        .andExpect(view().name(“home/home-view”))
        .andExpect(status().isOk()).andDo(print());

        that’s it! 🙂

        • cosenonjaviste

          Grazie del consiglio da provare sicuramente!

  • Pingback: Maven Integration Tests :: CoseNonJaviste()

  • Pingback: Testare API REST con Restfuse – Antonio Scatoloni()