|
Vediamo ora un esempio che mette in pratica quanto spiegato sino ad ora e che mostra l’efficacia della strategia di test appena presentata.
Esistono diversi e ottimi framework open source di testing per lo UTDD come JUnit, SOAP-UI, dbUnit, JSunit , Selenium, ecc …
Anche per l’ATDD fortunatamente esistono diversi (ottimi) framework: i due principali sono FIT/Fitnesse e Robot Framework.
In questo esempio utilizzeremo i framework di test open source JUnit e Fitnesse. 
Figura 4 - Fitnesse & JUnit Supponiamo di dovere sviluppare un servizio che permetta la gestione dei prelievi da un conto corrente bancario.
Ipotizziamo che le specifiche dei requisiti siano in forma di user story: “Come cliente della Banca voglio essere in grado di prelevare denaro dal mio conto corrente per avere disponibilità liquida”.
Bene, facile! No?
Ok, come potrebbe essere la firma del nostro metodo di business?
Primo tentativo: uno sviluppatore potrebbe creare un metodo che prevede il versamento e che in caso di errore effettui il solo log dell’errore senza ritornare alcun esito al client:
public void deposito (double sum) { . . . } // :-( !
Oppure: si potrebbe sviluppare un metodo, che in modo “old fashion”, preveda come risultato dell’operazione una codifica dell’esito dell’operazione.
public int deposito (double sum) { . . . } // :-(
Altra via: si potrebbe prevedere che il metodo sollevi un’eccezione nel caso in cui l’operazione fallisca
public void deposito (double sum) throws AccountException{ . . . } // :-|
Riproviamo ancora: prevedere anche di restituire il saldo aggiornato dopo l’operazione
public double deposito (double sum) throws AccountException{ . . .} // :-) E per quanto riguarda l’implementazione ?
Il requisito non esplicita in che modo si deve comportare il servizio nel caso in cui la cifra da prelevare sia zero o addirittura negativa. Che tipologia di messaggi di errore si vuole? Con che grado di dettaglio? Ci sono particolari informazioni che si reputa siano importanti da fornire per comunicare il fallimento dell’operazione ?
Con l’ATDD possiamo anticipare tutti questi aspetti che altrimenti sarebbero visibili solo in fase di UTDD del software o addirittura a sviluppo ultimato; in entrambi i casi si perderebbe tempo prezioso in dispendiosi ricicli di lavoro.
Utilizzando un framework ATDD come Fitnesse possiamo definire il comportamento (behaviour) del software che vogliamo venga realizzato. Nel nostro caso questo si traduce nel condividere con una semplice tabella in formato Word cosa si vuole ottenere come risultato atteso a fronte di determinati input.
Nel documento Word possiamo inserire tutte le spiegazioni del caso (commenti esplicativi e di testo narrativo) basta che il documento contenga delle semplici tabelle (chiamate fixture table) in cui si enfatizzano i dati di input ed il risultato atteso del software che si sta commissionando.
Figura 5 - Tabella Word per descrizione Acceptance Si noti come la tabella della figura 5 sia assolutamente chiara e comprensibile anche a persone di estrazione non tecnica e che si sta utilizzando Microsoft Word e non file di properties, XML, Java o altro.
Le uniche informazioni tecniche che “sporcano” il documento sono:
- la prima riga che deve contenere il nome della classe Java (Fixture class) che verrà sviluppata per soddisfare il test
- le colonne che devono indicare i nomi dei dati di input ed il risultato atteso (seguito da aperta e chiusa parentesi)
Tutto qua.
Automaticamente l’engine di Fitnesse invocherà la classe Java indicata nella tabella (Fixture class) kok.atdd.account.AccountColumnFixtureATDD. Se il nome del dato della tabella non è seguito da parentesi, l’engine Fit inietterà il valore della colonna in una proprietà pubblica della Fixutre class avente lo stesso nome (numeroContoCorrente e sommaDaPrelevare). Se il nome è seguito da parentesi, l’engine Fitnesse cercherà di invocare un metodo con quel nome della classe AccountColumnFixtureATDD.
Più facile a farsi che a dirsi.
Nel nostro caso lo sviluppo si traduce nella seguente classe Java:
public class AccountColumnFixtureATDD extends fit.ColumnFixture { /** Input */ /** Numero del conto corrente */ public String numeroContoCorrente;
/** Somma da prelevare dal conto corrente */ public String sommaDaPrelevare;
/** Output */ /** Messaggio di errore nel caso l'operazione non vada a buon fine */ public String messaggioErrore = null; /** * Ritorna il saldo aggiornato a fronte del prelievo * @return il saldo aggiornato * @throws Exception AccountException con il messaggio di errore */ public double saldoOperazione() throws Exception{ << logica di test >>
} public String messaggioErrore(){ . . . . } }
Nel metodo saldoOperazione() c’è il codice di test della nostra classe Java di Business Account:
public double saldoOperazione() throws Exception{ try{ Account account = new Account(numeroContoCorrente); double res = new AccountService().withdraw(account, new Double(sommaDaPrelevare)); return res; } catch(Exception e){ messaggioErrore=e.getMessage(); throw e; }
}
L’immagine che segue mostra la corrispondenza tra la tabella word (chiamata in nomenclatura Fitnesse: fixture) e la classe Java AccountColumnFixtureATDD.

Figura 6 - Tabella Word per descrizione Acceptance Test Adesso dobbiamo riempire i metodi di test con il codice di test.
Ecco il metodo saldoOperazione():
/** * saldoOperazione: test del metodo AccountService.withdraw() * @return il saldo aggiornato a fronte del prelievo * @throws Exception AccountException con il messaggio di errore */ public double saldoOperazione() throws Exception{ try{ String methodName = CLASS_NAME + ".saldoOperazione: "; Account account = new Account(numeroContoCorrente, 1000);
Double res = new AccountService().withdraw(account, new Double(sommaDaPrelevare));
return res.doubleValue(); } catch(Exception e){ messaggioErrore=e.getMessage(); throw e; } }
La classe AccountService, seguendo il mantra TDD è una classe che semplicemente non fa niente ma è compilabile!
public class AccountService {
private static final String CLASS_NAME = "AccountService"; /** prelievo * @param somma il valore della somma da prelevare * @throws AccountException se l'operazione non è fattibile */ public Double withdraw(Account acc, Double sommaDaPrelevare) throws AccountException{ return new Double(12345); }
Eseguendo il test con Fitnesse
%JAVA_HOME%\bin\java -classpath .;fitlibrary.jar;fitnesse.jar fit.FileRunner account_userstory.htm resultsAccount.html
ovviamente i risultati del test sono (giustamente) tutti negativi! 
Figura 7 - Tabella Word per descrizione Acceptance Test Bene, adesso possiamo concentrarci sul codice di business del metodo AccountService.withdraw().
Ecco il codice che permette di visualizzare tutti i test:
/** prelievo * @param somma il valore della somma da prelevare * @throws AccountException se l'operazione non è fattibile */ public Double withdraw(Account acc, Double sommaDaPrelevare) throws AccountException{ String methodName = CLASS_NAME + ".withdraw: "; System.out.println(methodName + ": Account ID["+acc.getId()+"]-sommaDaPrelevare["+sommaDaPrelevare+"]...");
double somma = sommaDaPrelevare.doubleValue(); double saldo = acc.getSaldo(); if (somma < 0){ String errMSg = methodName + ": somma da prelevare [" + somma +"]NEGATIVA!"; throw new AccountException(errMSg); } if (saldo < somma){ String errMSg = "#ERRORE# Somma da prelevare [" + somma +"] superiore al saldo[" + saldo +"]!"; throw new AccountException(errMSg); } saldo = saldo - somma; acc.setSaldo(saldo); return new Double(saldo); }
Rieseguendo i test otteniamo che i tre test danno esito positivo: il risultato ottenuto dalla classe Java AccountService è uguale al risultato atteso definito nella tabella Word.
Abbiamo finito?
No.
E’ vero che il nostro servizio si sta comportando in modo corretto e coerente con quanto richiesto dall’utente. La sua qualità esterna, cioè “ai morsetti”, è ottima visto che il codice sviluppato soddisfa tutto quanto concordato con il cliente.
Ma cosa dire riguardo la sua robustezza, cioè riguardo la sua qualità interna?
Il codice è robusto da un punto di vista tecnico? E’ facilmente manutenibile? Nel caso di bug fixing sarà facile effettuare la problem determination?
In altre parole, com’è la sua qualità interna ?
Il TDD con i suoi test di unità (UTDD) ci permette di valutare tutti questi aspetti.
Prepariamo dei test “interni” del servizio e verifichiamo che nel caso di dati di input non corretti il servizio risponda in modo chiaro.
Sviluppiamo alcuni test JUnit che oltre prevedere i dati corretti prevedano anche i dati non corretti, come ad esempio che la somma da prelevare o che l’account siano null.
Ad esempio: i seguenti test che verificano la robustezza del servizio danno rosso:
public void testPrelievoAccountNull(){ try { accountService.withdraw(null, new Double(500)); fail("Il test doveva fallire !"); } catch(AccountException e){ System.out.println(e.getMessage()); } catch (Throwable t){ fail(t.getMessage()); t.printStackTrace(); } } public void testPrelievoZero(){ try { accountService.withdraw(account, new Double(0)); fail("Il test doveva fallire !"); } catch(AccountException e){ System.out.println(e.getMessage()); } catch (Throwable t){ fail(t.getMessage()); t.printStackTrace(); } }
public void testPrelievoSommaNull(){ try { accountService.withdraw(account, null); fail("Il test doveva fallire !"); } catch(AccountException e){ System.out.println(e.getMessage()); } catch (Throwable t){ fail(t.getMessage()); t.printStackTrace(); } }
public void testPrelievoAccountNullSommaNull(){ try { accountService.withdraw(null, null); fail("Il test doveva fallire !"); } catch(AccountException e){ System.out.println(e.getMessage()); } catch (Throwable t){ fail(t.getMessage()); t.printStackTrace(); } }
I test danno rosso e mettono in rilievo una poca robustezza del codice.
Figura 8 - Tabella Word per descrizione Acceptance Test Si evince che la classe AccountService non effettua alcun controllo sulla validità dei dati in ingresso.
In caso di dati non validi il servizio cade miseramente in NullPointerException senza rispondere con messaggi chiari.
Avendo individuato la criticità possiamo provvedere a modificare il nostro codice affinché i test diventino verde.
Procediamo quindi ad inserire all’ingresso del metodo i controlli della validità sintattica e semantica dei dati di input in modo da avviare l’elaborazione di business se e solo se i dati sono corretti (inutile impegnare risorse preziose come CPU, RAM, rete, database nel caso di dati errati).
Ecco il codice modificato che permette di rendere verdi tutti i test:
/** prelievo * @param somma il valore della somma da prelevare * @throws AccountException se l'operazione non è fattibile */ public Double withdraw(Account acc, Double sommaDaPrelevare) throws AccountException{ String methodName = CLASS_NAME + ".withdraw: ";
if(acc == null){ String errMSg = "#ERRORE# Account is NULL!"; System.err.println(methodName + errMSg); throw new AccountException(errMSg); }
if(sommaDaPrelevare == null){ String errMSg = "#ERRORE# Somma da prelevare is NULL!"; System.err.println(methodName + errMSg); throw new AccountException(errMSg); }
System.out.println(methodName + ": Account ID["+acc.getId()+"]-sommaDaPrelevare["+sommaDaPrelevare+"]..."); double somma = sommaDaPrelevare.doubleValue(); double saldo = acc.getSaldo(); if (somma <= 0){ String errMSg = methodName + ": somma da prelevare [" + somma +"]NEGATIVA!"; System.err.println(errMSg); throw new AccountException(errMSg); } if (saldo < somma){ String errMSg = "#ERRORE# Somma da prelevare [" + somma +"] superiore al saldo[" + saldo +"]!"; System.err.println(methodName + errMSg); throw new AccountException(errMSg); } saldo = saldo - somma; acc.setSaldo(saldo); return new Double(saldo); }
Figura 9 - Tabella Word per descrizione Acceptance Test Adesso si che ci siamo!
Anche la qualità “interna” è OK.
E’ importante notare come i test UTDD siano di competenza dello sviluppatore che può quindi decidere di esternalizzare i dati del test in file di properties e/o XML e/o DB ecc … o decidere, per i dati volutamente non corretti, di cablarli nel codice.
A questo punto possiamo provvedere a rilasciare il software con una qualità esterna (test ATDD) ed interna (UTDD) che ci permetta di essere confidenti sulla bontà del nostro lavoro.
L’esempio utilizzato in questo articolo si è basato su un caso reale di realizzazione di Web Service (semplificato per questioni di chiarezza e di spazio) ma è applicabile anche nel caso di Applicazioni Web, ad esempio utilizzando il framework di test Selenium.
public class AccountWebTest extends SeleneseTestCase {
. . . .
public double saldoOperazione() throws Exception{ try{
selenium = new DefaultSelenium("127.0.0.1", 8666, "*firefox", "http://localhost:8080/"); selenium.start(); selenium.open("/account/"); selenium.typeKeys("accountId", "TST123"); verifyTrue(selenium.isElementPresent("j_id_jsp_saldo")); . . .
Un’ultima nota prima di concludere. Si è visto che la tabella Word utilizzata per specificare i test di accettazione ed i relativi criteri è “user-friendly”, in quanto di facile interpretazione anche per personale non tecnico.
La sua forma tabellare può essere resa ancora più user-friendly utilizzando linguaggi Domain Specific Language, cioè linguaggi di tipo dichiarativo adatti ad uno specifico dominio, allo scopo di poter specificare i requisiti in linguaggio di business, cioè nel linguaggio “naturale” dello specifico dominio.
Fitnesse permette di specificare un “Testing Domain Specific Language” in modo facile ed intuitivo mediante la classe DoFixture, estendendo la classe fitlibrary.DoFixture ed alternando nella cella delle tabella gli argomenti ed i relativi valori.
Ad esempio, la seguente tabella specifica gli argomenti “preleva dal conto”, “euro” e “verifica saldo finale” con i rispettivi valori: TST124, 100 e 900. 
Lato Java la classe di Test estende la classe DoFixture e mette a disposizione il metodo con il nome degli argomenti concatenati tra di loro; nel nostro esempio il nome del metodo è: prelevaDalContoEuroVerificaSaldoFinale() con tre argomenti che indicano rispettivamente il conto, la somma e il saldo da verificare.
public class AccountDoFixtureATDD extends fitlibrary.DoFixture {
/** * prelevoEuroDalContoIlSaldoDeveEssere * @param somma * @param conto * @param saldo * @return res */ public boolean prelevaDalContoEuroVerificaSaldoFinale(String conto, double somma, double saldoDaVerificare)throws Exception {
|