Introduzione
La volta scorsa vi avevo lasciato il compito di creare i test per la nostra API REST e pensare alla pagina HTML che gestisce le chiamate al nostro back-end per le operazioni di
- creazione
- aggiornamento
- cancellazione
In questa puntata vediamo come abbiamo ampliato i nostri test e se riusciamo a farli superare alla nostra pagina di visualizzazione delle “Cose da fare”.
I test delle API
Lo ammetto: in tutti gli anni di programmazione che ho alle spalle, mi sono sempre detto di dedicare più tempo alla scrittura dei test per le parti di codice che stavo sviluppando. Confesso di non sono mai stato molto prolifico sotto questo punto di vista, vuoi per alcuni posti di lavoro dove la parola test era sinonimo di perdita di tempo, vuoi perché, quando è il momento di scriverne uno, mi domando sempre “Bene! Ma che test devo fare?”.
Ecco perché in questa serie di articoli, oltre che a illustrare Vert.x, voglio mettermi di piglio buono a scrivere tutti i test necessari, mock eventuali compresi. Come per la volta scorsa, potete trovare il codice nel mio repository di GitHub [1] nel branch “test“.
Iniziamo: la pagina HTML e la API REST
Al momento l’unica parte di codice che possiamo testare è relativa alla pagina HTML e alla nostra piccola API REST.
Per prima cosa vediamo come il nostro toolkit ci aiuta nella stesura dei test sia di unità che di integrazione. Vert.x permette di utilizzare diversi stili per la parte dei test e di integrare framework di test come JUnit [2], molto comodo nel caso di uso di un qualche IDE.
Per iniziare con il nostro primo test, definiamo la classe TodoWebVerticleTestCase.java sotto il percorso:
src/test/java
Utilizzeremo ora un runner (VertxUnitRunner) per il framework JUnit che il toolkit ci mette a disposizione e che permette di avere con facilità il nostro accesso al contesto vertx.
La nostra classe di test segue la lo stile di una normale classe di test di JUnit con le annotazioni e gli Assert tipici del framework di test.
I due metodi annotati con @Before e @After servono rispettivamente a fare il deploy del nostro Verticle sottoposto a test e a rilasciare il contesto una volta conclusi i nostri test.
Test per la pagina index
Il primo test prevede di chiedere al nostro server la pagina index.html. Per verificare che sia proprio la nostra pagina cercheremo il titolo all’interno della response del server.
Il tutto risulta molto semplice. Il nostro toolkit mette a disposizione un semplice client HTTP senza dover utilizzare librerie esterne o le API messe a disposizione di Java.
@Test public void whenRequestRootReturnIndexPage(final TestContext context) { final Async async = context.async(); vertx.createHttpClient().getNow(HTTP_PORT, "localhost", "/", response -> response.handler(body -> { context.assertTrue(body.toString().contains("Todo Vert.X App")); async.complete(); })); }
Come possiamo vedere il tutto si traduce nella scrittura di un metodo annotato con @Test e utilizzando il il metodo getNow del client HTTP che Vert.x permette di creare tramite il metodo createHttpClient(). La risposta viene elaborata tramite un handler che, come vediamo, non è che una lambda che Java mette a disposizione dalla versione 8 e che permette di ridurre il codice scritto.
Testiamo la nostra API Rest
Anche per la nostra API Rest ci serviamo del client HTTP che Vert.x ci mette a disposizione e cominciamo a verificare il metodo per crear nuovi oggetti TODO.
Come nel test per la pagina, creiamo il nostro contesto asincrono in maniera tale da non occupare il mail loop di vertx durante le nostre prove e cominciamo a definire nel metodo annotato con @Before un oggetto TODO come possiamo veder nelle righe qui sotto.
final UserModel userModel = new UserModel(); userModel.setName("Marco"); userModel.setSurname("Rotondi"); userModel.setEmail("email@email.it"); userModel.setUsername("mrc"); userModel.setPassword("secret"); todoModel = new TodoModel(); todoModel.setTodoText("Appointment with All"); todoModel.setUser(userModel);
Il nostro oggetto todoModel è stato definito a livello di classe come privato in maniera tale da non doverlo creare ad ogni test.
Ora scriviamo il nostro test che non farà altro che postare il JSon del nostro oggetto come body della richiesta e verificherà che la risposta abbia effettivamente eseguito la creazione dell’oggetto.
@Test public void createNewTodo(final TestContext context) { final Async async = context.async(); final String bodyData = Json.encodePrettily(todoModel); final String bodyLength = Integer.toString(bodyData.length()); vertx.createHttpClient().post(HTTP_PORT, "localhost", "/api/todo") .putHeader("content-type", "application/json") .putHeader("content-length", bodyLength) .handler(response -> { context.assertEquals(response.statusCode(), 201); context.assertTrue(response.headers().get("contenttype"). contains("application/json")); response.bodyHandler(body -> { final TodoModel todo = Json.decodeValue(body.toString(), TodoModel.class); context.assertEquals(todo.getTodoText(), "Appointment with All"); context.assertNotNull(todoModel.getId()); async.complete(); }); }) .write(bodyData) .end(); } }
Possiamo vedere come nel precedente test che il toolkit di Vert.x facilita le cose grazie ai metodi che la classe Json mette a disposizione per serializzare e deserializzare gli oggetti.
Serializzazione/deserializzazione
Ora, se proviamo a lanciare il nostro test, otteniamo… un errore proprio sul test appena creato. Il messaggio di errore indica che non riesce a convertire il campo creationDate delle nostre classi TodoModel e UserModel.
Purtroppo la serializzazione e deserializzazione di un oggetto LocalDateTime non è supportato direttamente dal toolkit, che comunque permette di risolvere questo problema fornendoci un classe base da estendere per creare il nostro serializzatore/deserializzatore custom.
La cosa facile è che per utilizzare i nostri nuovi oggetti non serve scrivere configurazioni, ma semplicemente annotare i nostri Model con le nuove classi.
Il codice lo potete trovare nelle classi che riportiamo di seguito: per prima la LocalDateDeserializer.java.
public class LocalDateDeserializer extends StdDeserializer<LocalDateTime> { protected LocalDateDeserializer() { super(LocalDateTime.class); } @Override public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { String text = jsonParser.readValueAs(String.class); if (text == null) { return LocalDateTime.MAX; } else { return LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); } } }
E poi la LocalDateSerializer.java.
public class LocalDateSerializer extends StdSerializer<LocalDateTime> { protected LocalDateSerializer() { super(LocalDateTime.class); } @Override public void serialize(LocalDateTime value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { String format = value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); jsonGenerator.writeString(format); } }
Il tutto, come si vede dal codice, si traduce nel formattare la data in un formato comune e generare o l’oggetto o la stringa Json corrispondente.
Possiamo inserire le annotazioni nei nostri Model a livello di proprietà oppure a livello di getter e setter.
@JsonSerialize(using = LocalDateSerializer.class) public LocalDateTime getCreationDate() { return creationDate; } @JsonDeserialize(using = LocalDateDeserializer.class) public void setCreationDate(LocalDateTime creationDate) { this.creationDate = creationDate; }
Personalmente, preferisco metterle separatamente sui getter/setter.
A questo punto, rilanciando il test ci troviamo in una situazione Green (TTD docet…).
Aggiungiamo gli altri test
Il test successivo è quello di leggere tutta la nostra lista e sulla falsariga del test precedente: ribadiamo la semplicità di scrivere questo tipo di test.
@Test public void loadAllTodo(final TestContext context) { final Async async = context.async(); vertx.createHttpClient().get(HTTP_PORT, "localhost", "/api/todo").handler(response -> { context.assertEquals(response.statusCode(), 200); context.assertTrue(response.headers().get("contenttype") .contains("application/json")); response.bodyHandler(body -> { final TodoModel[] todoArray = Json.decodeValue(body.toString(), TodoModel[].class); context.assertEquals(todoArray.length, 1); context.assertTrue(todoArray[0].getTodoText() .equalsIgnoreCase(todoModel.getTodoText())); async.complete(); }); }).end(); }
Rilanciamo i nostri test e — sorpresa! — ci troviamo che il test di lettura fallisce. In questo caso i problemi sono principalmente due:
- non avendo ancora un datastore da cui attingere i dati, ad ogni lancio ci ritroviamo senza dati;
- il rischio è quello di avere l’esecuzione dei test non nella sequenza voluta e quindi il test fallirebbe comunque se leggiamo i dati prima di scriverli.
Refactoring!
Per risolvere questi problemi ci dedichiamo ad un po di refactoring del codice. Per prima cosa rinominiamo i metodi di test aggiungendo un prefisso che permetta un ordinamento del lancio dei metodi tipo a_, b_, c_ . In seconda battuta, diciamo a JUnit di eseguire i test secondo il nome dei metodi: aggiungiamo l’annotazione @FixMethodOrder(MethodSorters.NAME_ASCENDING) dopo il nostro runner.
Per risolvere il problema dei dati, facciamo qualche modifica al nostro test inserendo un metodo init() annotato con @BeforeClass dove inizializziamo vertx e facciamo il deploy del nostro Verticle in maniera tale da crearlo alla creazione della classe e mantenere lo stato nell’esecuzione dei test successivi.
Modifichiamo anche il metodo tearDown(), cambiando l’annotazione da @Before a @AfterClass in maniera di eseguire la chiusura corretta a fine test.
Alla fine di queste modifiche al codice, il test sarà passato.
Conclusioni
Vi invito a scaricare il progetto e dare un’occhiata al codice che trovate sotto il branch test, per avere un insieme completo dei test. Ci manca ancora la nostra pagina per visualizzare il tutto, ma con i test funzionanti possiamo dedicarci nella prossima puntata alla persistenza dei dati e a vedere un po’ di integrazione.
Se qualcuno nel frattempo ha voglia di presentare qualche idea per la homepage… come dire? Siete tutti benvenuti.
Ho cominciato con l'informatica da piccolo, prima con un MSX 2 e poi partendo da un 286 fino alle macchine dei giorni nostri.
Dopo gli studi di informatica, ho cominciato seriamente a lavorare prima con compiti di sistemista e gestione reti in una piccola realtà dell'altomilanese, e poi come come programmatore in varie società di consulenza per clienti medio-grandi.
Uso Java dalla versione 1.0... ma apprezzo anche altri linguaggi di programmazione come C# e php. Credo che non esista un linguaggio in generale migliore degli altri: semplicemente occorre essere bravi a scegliere quello che meglio aiuta a risolvere il problema nel migliore dei modi.