Test
Driven Development
Come suggerito dal nome, TDD è fondamentalmente una
pratica di sviluppo software incentrata sui test.
Seguendo tale pratica, lo sviluppo tradizionale viene "stravolto":
in TDD si sviluppa in primo luogo il codice di test della
funzionalità d'interesse, poi il codice applicativo
che tale funzione implementa.
Solo (e solo se) si ha un test che fallisce, si potrà
procedere allo sviluppo del codice applicativo.
Il
TDD si basa su queste due regole:
- non
scrivere codice applicativo se non si dispone di un test
automatico che fallisce
- eliminare
le eventuali duplicazioni in modo tale da mantenere il codice
quanto più possibile semplice
I
passi che caratterizzano lo sviluppo di software secondo la
TDD sono:
- aggiungere
un test prima di codificare la funzionalità applicativa
- codificare
la funzionalità applicativa in una forma minimale
purchè compilabile
-
lanciare il test (che dovrà fallire)
- modificare
il codice applicativo che implementa la relativa funzionalità
- eseguire
nuovamente il test
- se
il test fallisce, bisogna provvedere alle relative modifiche
del codice applicativo (passo 4) e quindi rieseguire i test
(passo 5).
-
se il test ha successo
-
provvedere a "ripulire" il codice da eventuali
duplicazioni o statement non strettamente necessari
e rieseguire il test (passo 5)
- se
già stato fatto il passo 7.a, ritornare al passo
1 per lo sviluppo di una nuova funzionalità.
Questo
processo viene scandito da tre ben precise fasi: Rosso, Verde
e Refactor (vedere ([KBTDD]).
Rosso:
si deve scrivere un test che non funziona (passo 3, 6)
Verde: si fa in modo che il test funzioni sviluppando
l'opportuna logica applicativa (passo 7)
Rifattorizzazione: si eliminano le eventuali duplicazioni
e/o compromessi di codice creati durante la modifica del codice
applicativo "alla ricerca del Verde" (passo 7.b)
Figura 1 -Test driver Development
Data l'importanza dei test è pre-condizione del TDD
avere un framework di Test che permetta una codifica ed una
gestione agile dei test, ad esempio Junit (vedere [JUNIT]).
L'indice
di qualità del test è direttamente proporzionale
ai bachi trovati. Se i test non mettono in rilievo dei bachi,
molto probabilmente questo indica che i test non sono stati
fatti bene e non sono sufficientemente "cattivi".
Se
si è in presenza di un baco che non è stato
rilevato dalla classe di test, è fondamentale aggiungere
un test che rilevi il bug prima di rimuoverlo dal codice applicativo.
Questa linea guida (vedere [TFGL]) è molto importante
perché permette di far crescere in modo incrementale
la suite di test.
Un
esempio di TDD
Esemplificando
quanto detto fino ad ora immaginiamo di dovere scrivere una
classe che modelli un conto corrente di una Banca e che metta
a disposizione la funzionalità di versamento ed il
saldo.
Utilizzando TDD, il primo passo da fare è creare l'opportuno
test (AccountTest) che verifichi il comportamento "ai
morsetti" (black box) della classe applicativa (Account).
Nello
sviluppo della classe di test andremo a definire in modo programmatico
le API della classe Account (non ancora sviluppata!) ed identificheremo
le funzionalità che deve soddisfare (vedere [TDDNAT]
): di fatto un "design by test".
public
class AccountTest extends TestCase {
private Account account;
public
AccountTest(String name){
super(name);
}
protected void setUp(){
this.account = new Account();
}
protected void tearDown(){
this.account = null;
}
public static Test suite(){
TestSuite suite= new TestSuite("All
Account Tests");
suite.addTest(new AccountTest("testVersamento"));
return suite;
}
public void testVersamento(){
try {
account.versamento(1000);
this.assertTrue(account.getSaldo()
== 1000);
}
catch(AccountException e){
this.assertTrue("Versamento
rifutato: "+e.getMessage(),true);
}
}
Codificando il codice di test abbiamo ipotizzato che la classe
Account deve fornire due API: versamento() e getSaldo(). Per
eseguire il test è necessario compilare la classe AccountTest
e di conseguenza compilare anche la classe Account. La
prima scrittura della classe Account deve essere ridotta allo
stretto necessario alla compilazione ed all'esecuzione del
test.
public
class Account implements java.io.Serializable {
public Account(){}
public float gatSaldo(){
return 0;
}
public void prelievo (float sum) throws AccountException{}
public void deposito (float sum) throws AccountException{
}
Figura 2 - Le classi AccountTest & Account
A
questo punto è possibile mandare in esecuzione il test
versamento della classe AccountTest; il test dovrà
fallire in quanto l'assert del saldo
this.assertTrue(account.getSaldo()
== 1000);
non
è uguale a 1000 bensì è 0 (nella versione
attuale, la classe Account non gestisce il saldo del conto).
A questo punto è possibile intervenire sul codice applicativo.
Inseriamo quindi nella classe Account la proprietà
che gestisce il saldo e la logica applicativa che implementa
il versamento.
Supponiamo
di aggiungere il seguente codice al metodo versamento:
public
void versamento (float somma) throws Exception{
float
valore = somma;
this.saldo
= this.saldo + valore;
}
rieseguendo
il test, otterremo un esito positivo in quanto in questo caso,
l'assert del saldo=1000 sarà verificato.
A
questo punto "ripuliremo" il codice eliminando le
eventuali duplicazioni o gli statement non strettamente neccessari
per poi rieseguire il test.
Nel
nostro esempio elimineremo la variabile d'appoggio valore
(sostanzialmente inutile)
public
void versamento (float somma) throws Exception{
this.saldo
= this.saldo + somma;
}
A
questo punto potremo ripetere il ciclo per una nuova funzionalità
(nel nostro semplice esempio, potrebbe trattarsi della codifica
del metodo che permette il prelievo dal conto) aggiungendo
innanzitutto un nuovo test nella classe AccountTest.
La
figura 3 riassume i passi fino ad adesso affrontati.
Figura 3 - Esempio di Test Driven Development
(clicca per ingrandire l'immagine)
Esaminando
il codice applicativo sviluppato, si può notare come
il metodo versamento della classe Account non controlli il
valore delle somme da versare (ipotizziamo che debbano essere
strettamente positive).
Prima
di modificare il codice applicativo (la classe Account) scriviamo
un nuovo test che permetta di mettere in evidenza il bug (il
test dovrà quindi fallire).
Procediamo
quindi ad un nuovo metodo di test nel quale inseriremo il
versamento di una somma negativa:
public
void testVersamentoNegativo(){
try {
account.versamento(-100);
this.fail("Il
versamento illegale di -100 e' stato permesso !");
}
catch(AccountException e){
this.assertTrue("Versamento
illegale rifutato: "+e.getMessage(),true);
}
}
il
test fallirà mettendo in rilievo la scarsa robustezza
del metodo Account.versamento(). A
questo punto modificheremo il codice applicativo per rimuovere
il bug, poi ripeteremo il test. Questo procedimento andrà
ripetuto fino a che il test non darà esito positivo.
Il
test avrà successo a fronte della modifica del metodo
versamento() della classe Account introducendo il controllo
che la somma da versare sia strettamente positiva:
public
void versamento(float somma) throws AccountException{
if
(somma <= 0){
throw
new AccountException("Somma negativa: " + somma);
}
this.saldo
= this.saldo + somma;
}
Figura 3 - Passi da eseguire per la rimozione di un bug
(clicca per ingrandire l'immagine)
Conclusioni
Sviluppare secondo TDD, garantisce che per tutto il codice
sviluppato esista un test associato. Seguendo TDD si arriva
a realizzare, parallelamente al codice applicativo, una suite
di test completa; questo da un lato aumenta la confidenza
sulla robustezza del proprio codice sviluppato, dall'altro
"semplifica" il refactoring e l'introduzione di
nuove funzionalità (lo sviluppatore può sempre
contare su una suite consistente di test).
Dal
punto di vista del TDD, l'aspetto fondamentale è la
garanzia di avere, in ogni istante, test che permettano di
rilevare eventuali regressioni software.
TDD
aiuta inoltre a costruire la documentazione d'uso del codice
sviluppato, infatti i test, per loro natura, sono ottimi sample
d'uso delle classi e dei relativi metodi.
Ultima
considerazione: applicando TDD in modo pragmatico ed incrementando
a piccoli step lo sviluppo, è possibile tenere sotto
controllo lo sviluppo ed i test del progetto in modo preciso,
sistematico e continuo.
L'attenzione
ai test è tipica delle pratiche agili. TDD è
menzionato come pratica complementare dall'Agile Modeling
(vedere [PAM]) ed è in qualche misura compreso da XP
mediante la pratica chiamata "test-first design"
(vedere [TFD]).
Visto che gli sviluppatori non devono scrivere nessun codice
fino a quando non si abbia per questo un test che fallisce
e visto che questo modo di procedere non è facilissimo
da applicare in modo sistematico, XP individua nella pratica
di pair-programming (sviluppo in coppia) uno strumento per
disciplinare l'approccio TDD.
Bibliografia
[MOKAMET_2]
S. Rossini: Processi e metodologie di sviluppo(II)-Mokabyte
85 Maggio 2004
[SATDD] Scott W. Ambler: Test Driven Development:
http://www.agiledata.org/essays/tdd.html
[KBTDD] Kent Beck: Test Driven Development: By Example, Ed.Addison
Wesley
[PAM] Scott W. Ambler: The Practices of Agile Modeling (AM)
-
http://www.agilemodeling.com/practices.htm
[TFD] Michael Feathers : Test First Design -
http://www.xprogramming.com/xpmag/test_first_intro.htm
[TDDNAT] Dan North : Test-Driven Development Is Not About
Testing
http://www.junit.org/news/article/test_first/index.htm#NotAboutTesting
[TFGL] Sean Shubin : Test First Guidelines:
http://xprogramming.com/xpmag/testFirstGuidelines.htm
[JUNIT] http://www.junit.org/index.htm
|