mercoledì 8 aprile 2015

Il principio di VERITA - Parte III

Nel post precedente ho esaminato il problema dell'Eseguibilità, cioè del non avere condizioni a latere che impediscano l'esecuzione del test ogni volta che viene eseguito.

Ripetibilità

Il terzo elemento è quello della Ripetibilità. In altre parole un test deve potere essere ripetuto un numero indefinito di volte senza che le precedenti esecuzioni possano in alcun modo condizionare l'esecuzione in corso.
Un test Ripetibile produce gli identici risultati ogni volta che viene eseguito. Deve essere quindi isolato rispetto ad ogni contesto che non sia sotto il controllo del programmatore.
Nessun progrmamma, in realtà, opera in isolamento, altrienti sarebbe probabilmente inutile. Ogni programma interagisce con altri sistemi che possono essere database, file, esseri umani, altri programmi.
Per ottenere l'isolamento occorre applicare diversi accorgimenti:
  • se si opera su database, il database va riportato ad uno stato iniziale sempre identico ogni volta che inizia il test. Non sempre è completamente possibile, ma è necessario fare tutto il possibile per evitare che precedenti operazioni CRUD possano inficiare il risultato del test;
  • i sistemi con cui il programma interagisce e che non possono essere condotti ad uno stato iniziale predefinito devono essere "mockati", cioè simulati da oggetti mock che si comportino sempre in modo uguale ogni volta che vengono richiamati;
  • evitare test che possano generare degli "attenti al lupo", cioè falsi positivi (errori che in realtà errori non sono) per colpa di una sottovalutazione delle conseguenze di un cambiamento dell'environment di cui non si è tenuto conto nella progettazione del test;

venerdì 23 gennaio 2015

Il principio di VERITA - Parte II

Nel post precedente di questa serie ho trattato la necessità di fare test veloci, per evitare che si smetta di eseguirli perchè il tempo complessivo di esecuzione della test suite è eccessivo.
Adesso vediamo il secondo punto.

Eseguibilità

I test devono essere eseguibili, cioè non vi devono essere condizioni a latere che possano in qualche modo impedire l'esecuzione dei test. Il che significa che:
  • i test non devono dipendere in alcun modo dal sistema operativo in cui la test suite gira
  • i test non devono essere dipendenti da una configurazione, a meno che la stessa non sia impostata all'interno della test suite stessa
  • se i test coinvolgono una dipendenza esterna, questa deve essere impostata dalla suite di test e non deve essere data per scontata. Il caso classico riguarda i test che implicano l'accesso a un database. In questo caso 
    • il database deve essere in memory, oppure in modalità embedded e avviato dalla test suite stessa;
    • i dati all'interno del database devono essere caricati nella fase di setup della test suit
In conclusione, quando progettate i test chiedetevi sempre se chi li eseguirà dovrà fare delle operazioni preliminari di setup prima di poterli eseguire. Se sì, c'è qualcosa che non va. Analizzate le dipendenze mal impostate e correggete l'errore.

lunedì 12 gennaio 2015

Il principio di VERITA - parte I

Una corretta ingegneria di un sistema di test in grado di abilitare l'uso delle metodologie agili deve rispettare alcuni principi base. Per facilitare la memorizzazione di questi principi è opportuno, talvolta, utilizzaredegli acronimi. Quello di cui parlerò oggi è VERITA. Un sistema di test agile deve essere impostato secondo il principo di VERITA. Vediamo cosa significa:

VERITA

Seguendo i dettami del principio di verità, i test devono essere:

Veloci,
Eseguibili
Ripetibili
Isolati,
Tempestivi
Autovalidanti

Vediamo, punto per punto, cosa significa applicare questo principio.

Veloci

Una Test Suite per un'applicazioni di medie dimensioni può comprendere anche diverse migliaia di Test Unit. Se ogni test unit dovesse impiegare anche solo un secondo per essere eseguita, una Test Suite potrebbe richiedere più di un'ora. Il risultato sarebbe l'abbandono "de facto" della metodologia. Per evitare di sprecare ogni giorno più del 10% del proprio tempo i programmatori finirebbero col far girare solo le Unit più recenti, quelle che riguardano il solo codice in corso di sviluppo, dove più alto è il livello di attenzione.

Risultato: le regressioni sarebbero scoperte solo molto tardi, nello sviluppo. Se anche le cose fossero messe meglio, diciamo 3000 Unit Test da 100ms, sarebbero comunque 300 secondi, pari a 5 minuti. O avete un team di caffeinomani, oppure non riuscirete a convincere i vostri programmatori a interrompere le attività più volte al giorno ed aspettare 5 minuti perchè una Test Suite termini. Nessuno aspetta 5 minuti più volte al giorno.

Una corretto approccio Test Driven richiede che si scriva un po' di codice, poco, e che immediatamente si testi l'intera applicazione. Cercare di accumulare una grossa quantità di modifiche prima di eseguire i test porta a due comportamenti, entrambi deprecabili:
  1. viene committato (e/o pullato) codice non testato nel sistema di versioning, rendendo instabile l'ambiente di sviluppo condiviso;
  2. viene committato (e/o pullato) codice a grossi blocchi, complicando il merge e, di nuovo, rendendo instabile l'ambiente.
C'è poco da fare, l'unica soluzione è rendere veloci i test. Per ottenere questo risultato è indispensabile eliminare ogni dipendenza dei test da sottosistemi intrinsecamente lenti, come possono essere:
  1. database di vario tipo, NoSql compresi
  2. connessioni in rete aventi tempi di risposta impredicibili, come sono spesso i web service
  3. sistemi remoti che espongono API che erogano servizi in modo lento o impredicibile
Il primo compito di uno sviluppatore, quindi, sarà di minimizzare la dipendenza dei propri test da sistemi di questo tipo. La domanda, a questo punto, sorge spontanea.

"Ma se devo evitare di mettere sotto test le aree dei programmi che fanno accesso a sottosistemi esterni, come faccio a testare quelle parti dell'applicazione in cui vengono gestite proprio le interfacce?"

Vedremo in una specifica serie di post come risolvere questo problema tramite i Mock. Intanto facciamo proprio il primo principio, e impegnamoci a creare test veloci, eseguibili in al massimo poche decine di millisecondi, cercando di dare comunque la massima copertura funzionale del codice ma evitando di finire imbottigliati in dipendenze lente e imprevedibili.

venerdì 2 gennaio 2015

La gestione degli errori nella programmazione Agile

Nella programmazione Agile una delle scelte chiavi è come gestire gli errori tirati (thrown) dalle classi custom che compongono la propria applicazione.

Tranquilli, non voglio riprendere in mano la vecchia polemica tra eccezioni checked e non-checked. Da diversi anni (da Spring in poi, direi) i framework  tendono tutti a proporre eccezioni non-checked, ma ci sono in giro diversi programmatori convinti che in molti casi le eccezioni checked abbiano dei vantaggi.

Il Web straripa di commenti al riguardo. Giusto per farsi un'idea, per chi fosse nuovo a questa polemica, questo link è un buon punto di inizio.

Supponiamo di avere in corso lo sviluppo di una libreria (chiamiamola MyLibrary) che esegue delle operazioni che possono andare male, e che si ritenga che il modo migliore di comunicare con il layer superiore, quello che normalmente chiama i metodi della libreria, sia di tirare delle eccezioni unchecked. il mio suggerimento, derivato al 90% dai 6 tips di Dale Taylor, è il seguente:

Create una interfaccia, chiamatela MyLibraryErrorCode e fatela esporre il metodo getKey()

public interface MyLibraryErrorCode{
    public String getKey()
} 

Create una RuntimeException e nominatela in modo che sia evidente il fatto che tramite questa RuntimeException gestirete le eccezioni dell'intera vostra Library. 

Ovviamente, potete subclassare Exception se volete gestire eccezioni checked.  Oppure avere una famiglia di eccezioni checked ed una unchecked e gestirle entrambe con lo stesso approccio. Come già spiegato, lo scopo di questo post non è di contribuire alla diatriba tra checked e unchecked, ma solo proporre un approccio alla loro gestione.

public class MyLybraryException extends RuntimeException{

    private MyLibraryErrorCode errorCode;
    private Map<String,Object> attributes=new HashMap<String,Object>();


    public MyLibraryException(String message, MyLibraryErrorCode errorCode){
        super(message);
        this.errorCode=errorCode;
    }

    public MyLibraryException(MyLibraryErrorCode errorCode){
        this.errorCode=errorCode;
    }

    public MyLibraryErrorCode getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(MyLibraryErrorCode errorCode) {
        this.errorCode = errorCode;
    }
    public MyLibraryException set(String key,Object value){
        attributes.put(key,value);
        return (this);
    }

    public Object get(String key){
        return attributes.get(key);
    }
}

Tramite questa classe potrete creare RuntimeExceptions di tipo MyLibraryException dotate di un codice di errore di tipo MyLibraryErrorCode e di un array di key-value in grado di registrare una lista di attributi arbitrari che saranno utili per poter meglio documentare l'Exception nei layer superiori dell'applicazione.

Create degli Enum che implementino l'interfaccia MyLibraryErrorCode

Il codice sarà, ad esempio, il seguente:
public enum BasicsError implements MyLibraryErrorCode{
    NO_NUMERATOR,
    BAD_KEY,
    BAD_FORMAT  

public String getKey(){
        return this.getClass().getSimpleName()+"_"+this.toString();
    }
}

Nell'esempio della MyLibrary, quindi, tramite questo Enum gli errori classificati come BasicError verranno identificati da uno specifico codice (NO_NUMERATOR, BAD_KEY oppure BAD_FORMAT). La classe BasicError mette inoltre a disposizione un metodo (getKey) che sarà utile ad una rappresentazione compatta del codice di errore da utilizzare nella propria gestione multlingua degli errori.

Vediamo adesso un esempio di utilizzo di questo metodo.

Esempio di utilizzo  

Supponiamo di avere un punto nel codice della MyLibrary dove è necessario tirare delle eccezioni permettendo allo strato superiore del codice di costruire, a fronte delle stesse, una messagistica multilanguage chiara e dettagliata.

Un esempio potrebbe essere questo snippet:
 
    NumeratorFeeder nf = getNumeratorFeeder(code, date);
    if (nf == null)
        throw new MyLibraryException(BasicsError.NO_NUMERATOR)
        .set("code", code)
        .set("date", date);
    else {
       .......

In questo esempio si può osservare come:
  1. sia tirata una eccezione di tipo MyLibraryException;
  2. l'eccezione sia stata tipizzata cone NO_NUMERATOR
  3. il vettore attributes dell'eccezione sia stato alimentato con le key-value relative ai valori dei campi code e di date presenti al momento in cui è stata tirata l'eccezione.
 Il codice dello strato superiore potrebbe essere così organizzato:

try {
   assertEquals(234, numeratorRep.getNextNum("invNum", new DateTime(2999, 4, 12, 0, 0)));
} catch (MyLibraryException e) {
   if(e.getErrorCode().equals(BasicsError.NO_NUMERATOR)) {
      String italianMessage = context.getMessage(e.getErrorCode().getKey(), new Object[]{"invNum", new DateTime(2999, 4, 12, 0, 0).toString(DateTimeFormat.shortDate())}, Locale.ITALIAN);
      String englishMesage = context.getMessage(e.getErrorCode().getKey(), new Object[]{"invNum", new DateTime(2999, 4, 12, 0, 0).toString(DateTimeFormat.shortDate())}, Locale.ENGLISH);
      String itmsg = String.format("Nessun numeratore trovato per numeratore con codice %s alla data %s", e.get("code"), ((DateTime) e.get("date")).toString(DateTimeFormat.shortDate()));
      assertEquals(italianMessage, itmsg);
      String enmsg = String.format("No numerator found for code %s and date %s", e.get("code"), ((DateTime) e.get("date")).toString(DateTimeFormat.shortDate()));
      assertEquals(englishMesage, enmsg);
      }
}

Si noti quanto segue:
  1. l'eccezione tirata non è specifica per il caso NO_NUMERATOR, ma generica per tutta la MyLibrary. In questo modo si può evitare di dover definire decine di eccezioni custom con nomi fantasiosi. Basta un tipo di eccezione (due se si desidera avere eccezioni non-checked insieme a eccezioni checked);
  2. il motivo per cui è stata tirata l'eccezione è contenuto nell' ErrorCode
  3. insieme all'errorCode è possibile accedere all'array di key-value dove sono descritte le circostanze per cui l'errore è avvenuto. Ciò è particolarmente utile in elaborazioni batch dove l'errore può essere dovuto a dati malformati, key inesistenti, ecc.
  4. il messaggio di errore può essere così composto  in modo multi-language unendo un testo costante con i valori contenuti nel messaggio di errore.
Con questo approccio è stata costruita la libreria sottostante a Tyl Framework, basato su Vaadin e MongoDb,  che è orientato a fornire una piattaforma per lo sviluppo di applicazioni gestionali con metodi agili.

lunedì 29 dicembre 2014

Scrum nei microprogetti

Solitamente si crede che il processo Scrum debba essere utilizzato nei progetti medio-grandi, che coinvolgono almeno 7/10 persone e che durano diversi mesi.

Non è detto. Si possono applicare i principi fondamentali dello Scrum anche in micro-progettii, gestidi da una o due persone al massimo.
In questo caso le cerimonie tipiche dello Scrum (Daily meeting, Sprint planning, ecc.) non trovano applicazione, soprattutto se si tratta di un one-person-team.

Però valgono i seguenti principi:
  1. costruire un backlog in ordine di importanza rispetto all'obiettivo
  2. mantenere il ritmo di uno sprint ogni 2/3 settimane
  3. al termine di ogni sprint rilasciare comunque qualcosa di funzionante, magari da migliorare e rivedere allo sprint successivo
  4. applicare metodi di sviluppo di tipo TDD
  5. utilizzare un sistema di Continuous Integration per garantirsi un'evoluzione del software al riparo da problemi di regressione.
Nei mesi di Novembre e Dicembre 2014 ho seguito due micro-progetti realizzati secondo questa tecnica. Si tratta di due add-on di Vaadin (https://vaadin.com/directory#addon/mongodb-container-addon e https://vaadin.com/directory#addon/fieldbinder-addon) realizzati da Edoardo Vacchi e Daniele Zonca.

Lavorando con metodo  Scrum, in due mesi hanno ottenuto un codice versionato su Github, dotato di test unit, sottoposto alla Continuous Integration di Travis-CI, documentato e supportato da un Tutorial.

In ambedue i casi si tratta di una release iniziale, da sviluppare ulteriormente, ma già perfettamente funzionante  e, nel caso del MongoDb container, già funzionalmente completa.