Introduzione
Nella precedente serie di articoli, dedicata ai microservizi [1], abbiamo visto un modo per suddividere un monolite in diversi microservice. Con questa nuova serie, vedremo come fare per dormire sonni più tranquilli ogni volta che dobbiamo intervenire sulla nostra code base.
Quali sono gli strumenti che ci che garantiscono che le nuove modifiche che realizzeremo non andranno a rovinare quanto di buono sino ad allora è stato costruito? Ma ovviamente i test!
Croce e delizia dei test
L’argomento dei test è veramente molto ampio e lo scopo di questa serie di articoli non è l’approfondimento di tutti i tipi di test o di ogni metodologia oggi esistente, ma bensì di introdurre al Test Driven Development (TDD) con i suoi concetti base per dare una visione di insieme e poter poi passare alla fase pratica. Alla fine di questo articolo abbiamo comunque riportato alcuni riferimenti bibliografici su cui potete approfondire per bene l’argomento.
Classificazione dei test
Come anticipato nel precedente paragrafo, esistono diverse classificazioni dei test e noi ci soffermeremo su una di esse per fare una breve introduzione a questo mondo: la classificazione piramidale.
Già dall’immagine si comprende che esistono tipi di test a tutti i livelli dell’applicazione ed emerge come il loro numero cresca a mano a mano che ci spostiamo dal vertice verso la base della stessa.
User Interface Test
I test che vengono implementati a questo livello hanno un ambito ampio e sono, di conseguenza, poco precisi. Quando fallisce uno di questi test non è immediatamente chiaro dove risiede il problema, dato che il test potrebbe potenzialmente utilizzare l’intero sistema e quindi l’errore potrebbe essere ovunque nell’applicazione e non necessariamente nell’interfaccia.
Altra complicazione legata ai test dell’interfaccia è che servono per verificare il comportamento della stessa e quindi non è sempre semplice la loro automazione.
Esistono principalmente tre tipi di approccio al testing della UI:
- manuale;
- cattura e riproduci;
- basato su modelli.
Test manuale
Il test manuale, come dice la parola stessa, è svolto da un operatore che di norma segue una lista di casi d’uso e verifica che il comportamento dell’interfaccia sia corretto.
Test cattura e riproduci
Nel secondo caso, invece, si usano software che permettono di registrare delle macro con i relativi stati in cui si devono trovare i componenti e che permettono di rieseguirli in automatico. Sul mercato esistono prodotti molto maturi, sia gratuiti che a pagamento, come Selenium [2], eggPlant [3] o Jubula [4] per citarne alcuni.
Test basato sui modelli
L’ultimo, e forse più complesso, è il testing basato su modelli. Il principio si basa sul derivare i casi di test da modelli che esprimono gli aspetti funzionali del sistema, definito di norma SUT (System Under Test) e rappresentato in questo caso proprio dalla nostra interfaccia. Il testing basato su modelli si divide in tre sottotipi, che non approfondiremo, ma che riportiamo per completezza, vale a dire basati su eventi, stati o modelli del dominio.
Service Level Test
I test a questo livello servono per verificare sia il singolo componente, come appunto un microservice, sia per testare il suo comportamento all’interno dell’intero sistema: “See the Whole” recita uno dei principi Lean.
Mock objects
Per fare ciò, senza però coinvolgere strati esterni, serve creare oggetti che ne mimino il comportamento, ma in modo controllato (mock objects). Per fare un esempio pratico, pensiamo a un servizio che legge e scrive in un database. Non vogliamo che i comandi di insert vadano effettivamente dentro il database in quanto non stiamo testando il suo funzionamento, altrimenti sarebbe quello che viene definito test di integrazione; pertanto, costruendo un mock che simula il DB, potremo creare situazioni in cui, alla chiamata di insert, questo ci avverta che tutto è andato a buon fine oppure restituisca uno specifico errore. Il tutto, appunto, senza dover allestire un DB vero e proprio e rimanendo molto veloci nell’esecuzione: è solo codice.
Unit Test
Lo scopo di questo tipo di test è quello di verificare il funzionamento corretto di ogni nostra classe e/o funzione ed è per questo che non dovrebbe mai varcarne i suoi confini e invece lavorare in completo isolamento. Questo proprio per garantire al programmatore la certezza che il suo codice funzioni.
Test Driven Development
Una delle tecniche che viene normalmente applicata è il TDD (Test Driven Development) dove sono i test stessi a pilotare lo sviluppo del codice; prima si scrive il test e poi il minimo codice necessario a soddisfarlo. Immaginate lo unit test come il “cosa” testare ed il TDD “quando” testare.
Il pattern per eccellenza è quello chiamato “red-green-refactor”. Si scrive un test che inevitabilmente fallisce, perché manca il codice o perché prevede dei cambi a funzionalità esistenti (Red). Si implementa la soluzione minimale che permette al test di passare (Green) e poi si esegue il refactoring del codice ove necessario.
I test prima del codice
Ma perché dovrei prima scrivere i test, e solo successivamente implementare il codice? Scrivere i test prima del codice aiuta a prendere confidenza con ciò che stiamo implementando, ci costringe a pensare prima al problema da risolvere e fa sì che i test non siano influenzati dal codice scritto in precedenza. Ovviamente, come tutte le cose, va usata cum grano salis.
I vantaggi del TDD
Abbiamo parlato del perché scrivere i test prima del codice, ma i vantaggi anche scrivendo i test successivamente al codice rimangono e sono a nostro avviso notevoli. Quelli che andremo a elencare di seguito sono le nostre personali conclusioni dopo anni di utilizzo e siamo certi che questa lista potrebbe crescere semplicemente raccogliendo le testimonianze di altri programmatori.
Qualità
La prima cosa che si noterà dopo qualche mese di utilizzo di queste tecniche è che la qualità del codice cresce implicitamente: scrivere test ci costringe a scrivere codice testabile, quindi piccolo, più modulare e di conseguenza più mantenibile.
Identificazione dei bug
Il tempo di identificazione dei bug si riduce considerevolmente: immaginiamo un cliente che ci segnala un baco, per esempio un errore di calcolo al verificarsi di determinate condizioni; senza un sistema di test dovremmo entrare nel sistema in produzione — o avere una sua copia aggiornata — e provare fisicamente a fare gli stessi passi per poi debuggare il codice relativo. Sarà invece sufficiente creare un test su misura e debuggare il codice, senza dover nemmeno avere la copia aggiornata del database o altro in quanto sarà sufficiente un mock su misura per simulare le stesse condizioni.
Semplificazione nelle modifiche
Inoltre si è molto più tranquilli quando si devono affrontare modifiche al sistema. Sapendo di avere il nostro software testato — se scriviamo codice solo se richiesto da un test, saremo sempre vicini un code coverage del 100% — queste modifiche si fanno con meno timore. Un’implementazione con effetti non previsti farà diventare rossi alcuni test facendo emergere istantaneamente il problema. Questo, ovviamente, non vuol dire fare le cose senza metterci testa.
Il costo del TDD
Scrivere test a supporto del proprio codice ha una pletora di vantaggi, ma ovviamente c’è l’altro lato della medaglia… i test non si scrivono da soli. E questo si riassume in tempo in più nello sviluppo. Per nostra esperienza personale, i primi tempi avevamo registrato un 35-40% in più nel tempo di sviluppo e dopo qualche mese questo margine si era abbassato intorno al 20%, ma quel 20% in più rimane e non va via.
Pertanto è assolutamente importante ricordarsene e adeguare le proprie stime quando si valuta l’entità di un lavoro, che sia un nuovo progetto o semplicemente un’integrazione. Nel secondo caso potrebbe volerci ancora di più in quanto potremmo trovaci a dover modificare di molto il codice per renderlo testabile, specie con vecchie applicazioni.
Un altro aspetto da non sottovalutare, specialmente all’inizio, quando si sta cercando di impostare un nuovo metodo di lavoro, è che la “pigrizia” arriva camuffata in molti modi: “ho poco tempo”, “non è importante testare questa parte” etc. La tentazione di non scrivere test è forte. Se però si ha un metodo ormai impostato, è facile accorgersene e correggerlo.
Conclusioni: un primo esercizio
Nei riferimenti bibliografici, riportiamo una serie di titoli [5] [6] [7] [8] che possono aiutare ad approfondire approfondire i concetti esposti superficialmente in questo primo articolo introduttivo.
I più diligenti e studiosi, poi, potranno provare a “sporcarsi le mani” da soli con un kata [9] che trovate al link [10] nei riferimenti bibliografici e che potete fare in autonomia
In ogni caso, lo scopo dei prossimi articoli, è quello di guidarvi nel testing di un caso più complesso.
Riferimenti
[1] Alberto Acerbis, Uno sguardo ai microservizi – I parte: Introduzione e panoramica. MokaByte 223, dicembre 2016
http://www.mokabyte.it/2016/12/microservizi-1/
[2] SeleniumHQ, browser automation
[3] eggPlant, test automation tools
https://www.testplant.com/eggplant/testing-tools/
[4] Jubula, the functional testing tool
[5] Kent Beck, Test-Driven Development: By Example. Addison Wesley, 2002
[6] Roy Osherove, The Art of Unit Testing: with examples in C#. Manning Publications, 2013
[7] Robert Martin, Clean Code: A Handbook of Agile Software Craftsmanship. Prentice Hall, 2008
[8] Steve Freeman – Nat Pryce, Growing Object-Oriented Software, Guided by Tests. Addison Wesley, 2009
[9] La voce “Kata (programming)” su Wikipedia
https://en.wikipedia.org/wiki/Kata_(programming)
[10] Kata FizzBuzz su Coding Dojo