Aspetti
generali
Vediamo prima di tutto di capire come mai si tende (o
si tendeva?) a non considerare in modo più approfondito
l'attività di test come parte dello sviluppo.
Alla base di tutto c'è probabilmente una grave
lacuna nell'insegnamento della programmazione. Per praticità
i primi esercizi di programmazione si concentrano su
problemi di matematica, da trattare numericamente. Risolvere
un'equazione o un sistema di equazioni porta però
a un numero di soluzioni limitate, compresi i casi particolari.
Sviluppare software porta invece a un numero maggiore
di soluzioni (errori compresi) e a un numero di casi
particolari che, a dipendenza dal numero di errori,
può andare da zero... all'ingestibile.
Chi insegna i fondamenti di informatica deve portare
agli allievi non solo concetti teorici, ma anche un'esperienza
pratica di sviluppo software, introducendo cioè
non solo i concetti di programmazione, ma anche metodologie
di test.
Bisognerebbe insegnare che il ciclo di vita di un programma
non termina alla sua prima consegna. Al contrario, quello
è il suo punto di inizio. Da lì in poi
il programma evolverà, crescerà, subirà
modifiche e manutenzioni di vario tipo. Il test è
importante durante lo sviluppo, ma lo è ancora
di più durante i successivi cicli di evoluzione.
Fretta
e termini di consegna
Chiunque si sia trovato a lottare con termini di consegna,
sa esattamente di cosa stiamo parlando.
Purtroppo con la fretta e i termini di consegna si rischia
presto di entrare in un circolo vizioso dal quale è
poi difficile uscire. Se per questioni di fretta si
tralascia di scrivere codice di test, si rischia più
tardi di pagare la fattura. Un programma senza test
è infatti un programma potenzialmente poco flessibile
e quindi poco robusto ai cambiamenti. Modificare un
programma poco robusto diventa impresa ardua e lunga.
Questo farà diminuire la produttività,
che farà aumentare la fretta, ecc., ecc.
Col tempo nemmeno lo sviluppatore si fiderà del
programma e oserà apportare modifiche, perché
anche un piccolo cambiamento rischierà di provocare
situazioni inconsistenti nel codice esistente. Questo
rappresenta l'inizio della "morte" del programma.
Poca professionalità
Se ai due motivi precedenti aggiungiamo la poca professionalità,
ecco che otteniamo il quadro completo.
Poca professionalità non significa necessariamente
basso profilo di formazione. È vero che probabilmente
in nessun campo come in informatica esiste un così
grosso numero di "praticoni". Va però
anche detto che i praticoni potenzialmente più
pericolosi sono quelli che hanno una formazione adeguata
(affine all'informatica) abbinata a poca professionalità,
o comunque, nel caso specifico, alla presunzione che
il software che sviluppano non ha bisogno di verifiche.
Si ricorre qui al vecchio luogo comune del "dipartimento
di test", cioè quel fantomatico dipartimento,
presente, a quanto pare, in ogni azienda, responsabile
di verificare tutto il software che viene prodotto.
In altre parole, questa è la frase ricorrente:
"io scrivo il programma, poi ci pensino pure gli
altri a verificarlo". Alla faccia della professionalità...
Questo atteggiamento denota oltretutto confusione di
termini e di interpretazione tra test funzionale (functional
testing, di competenza di sviluppatori, apparati esterni
e committenti) e test di unità (unit testing,
di competenza prevalentemente degli sviluppatori).
Test
come attività di sviluppo
Il
test come attività di sviluppo a tutti gli effetti
non è certo una novità, basti pensare
alle metodologie di "extreme programming"
che posizionano il test addirittura al centro dello
sviluppo.
In Java la cosa è relativamente recente e può
essere fatta ricondurre alla diffusione di JUnit ([1]),
un framework che aiuta a gestire i test di unità,
realizzato in Java per programmi scritti in Java.
I vantaggi del test come attività di sviluppo
sono molteplici. Eccone elencati alcuni:
-
Accorcia i cicli di sviluppo intesi come cicli in
cui il codice cambia senza che alla fine il programma
si trovi in uno stato di inconsistenza.
- Aumenta l'autostima e la fiducia dello sviluppatore
nel codice che ha scritto, evitando la situazione
descritta in precedenza, in cui chi ha realizzato
il software non si azzarderebbe per nessun motivo
a modificarlo (funziona, lasciamolo così...).
- Facilita l'applicazione di metodi di refactoring([2]),
che sono l'essenza dell'evoluzione del software.
- Tutto questo, in ultima analisi, aumenta la produttività,
perché oltre ad evitare di entrare nel circolo
vizioso descritto in precedenza, predispone software
e sviluppatore al cambiamento. Ogni modifica potrà
quindi essere eseguita con meno ansia, sicuri che
i test scritti saranno in grado di determinare eventuali
situazioni di inconsistenza al termine dei vari cicli
di refactoring. (Oltre a ciò, non si è
più costretti a credere alla leggenda metropolitana
del dipartimento di test...).
JUnit
Se
parliamo di sviluppo e test in Java, non possiamo non
parlare di JUnit. Si tratta di un tool che facilita
l'accumulo di test da eseguire in modo automatico in
qualsiasi momento. Oltre a questo, JUnit mette a disposizione
un'interfaccia grafica che permette, a colpo d'occhio,
di sapere se le sequenze di test hanno dato risultati
positivi o negativi.
Al di là della funzionalità specifica,
il grosso merito di JUnit è però soprattutto
quello di avvicinare gli sviluppatori Java, di cui molti
provengono da C/C++, a metodologie di test presenti
in altri ambienti più avanzati, primo fra tutti
Smalltalk.
Il suo utilizzo è molto semplice, basta capire
cosa esegue e cosa mette a disposizione il framework.
Iniziamo da un semplice esempio numerico, tanto per
dimostrare che anche in questo campo il test è
utile.
Supponiamo di dover risolvere il seguente problema:
Definire in una classe, un metodo, che ricevuto un intero
in input restituisca la metà del suo argomento
se questi è pari o il triplo più uno se
invece è dispari.
Usarla per stabilire (altro metodo) quanto è
lunga la sequenza di applicazioni ripetute della funzione
fino ad ottenere 1 per un valore qualsiasi positivo
maggiore di 2 (valore: variabile di istanza inizializzata
dal costruttore della classe). Per applicazione ripetuta
si intende che il valore restituito da una chiamata
viene passato come argomento ad una chiamata successiva.
La lunghezza della sequenza trovata è da registrare
nell'oggetto e deve sempre essere possibile, attraverso
un metodo, richiamare in qualsiasi momento quasta lunghezza
(int getLength()) e il valore del numero che genera
una sequenza di questa lunghezza (int getValue()).
Esempio
di sequenza:
valore iniziale: 10
lunghezza della sequenza: 6 (10 5 16 8 4 2)
Vediamo
di realizzare una prima versione del metodo responsabile
di ogni singolo passo della sequenza, con la sua integrazione
nella classe Sequenza.
public
class Sequenza{
int valore, contatore;
public Sequenza(int v){
valore = v;
contatore = 0;
}
static int modifica(int val){
if (val%2!=0)
return val*3+1;
else
return val/2;
}
}
Il metodo modifica() è puramente funzionale,
per cui può essere definito come static. In generale
i metodi static possono diventare problematici quando
si lavora con threads, ma non è il caso nel nostro
problema.
Realizzato il primo metodo vediamo come verificarne
la correttezza.
Ogni metodo di test in JUnit dev'essere "public
void", non avere parametri e iniziare con "test",
questo per essere riconosciuto dal framework attraverso
il meccanismo di reflection:
public
void testModifica(){
assertTrue(Sequenza.modifica(3)
== 10);
assertTrue(Sequenza.modifica(2)
== 1);
assertTrue(Sequenza.modifica(1)
== 4);
assertTrue(Sequenza.modifica(0)
== 0);
assertTrue(Sequenza.modifica(-3)
== -8);
}
Si tratta semplicemente di richiamare la funzione e
confrontare il risultato con quello atteso.
Per il confronto del risultato il framework mette a
disposizione una lista di metodi assert, direttamente
utilizzabili, perché ereditati da TestCase, la
classe estesa dalla nostra classe SequenzaTest, che
si presenta quindi come segue:
public
class SequenzaTest extends TestCase{
public SequenzaTest(String title){
super(title);
}
public static Test suite(){
return new TestSuite(SequenzaTest.class);
}
public void testModifica(){
...
}
}
JUnit
può essere lanciato dalla linea di comando in
questo modo:
java
junit.swingui.TestRunner
Una
volta mostrata l'interfaccia e inserito il nome della
classe SequenzaTest (verificare che si trovi nel classpath),
JUnit mostra una barra verde orizzontale, a significare
che il test è corretto.
Ancora
due parole sul metodo suite(). Serve a creare una cosiddetta
TestSuite, o sequenza di implementazioni di Test, in
questo caso formata da un' unica classe. In generale
è possibile associare in una suite vari oggetti
di test, permettendo così di frammentare i test
di un progetto su più classi e scegliere in ogni
momento se chiamarli singolarmente o tutti assieme.
Il pattern utilizzato è il Composite ([3],[4]),
che rende trasparente il fatto che la suite sia formata
da uno o più oggetti di test. Non solo, ma la
struttura può essere gerarchica.
Test
first
Continuiamo
con il nostro esempio, utilizzando il procedimento "test
first", tanto caro ai seguaci di extreme programming
([5]). Si tratta in altre parole di definire prima il
test, quasi come specifica di cosa ci si aspetta e poi
scrivere il codice che permetta a JUnit di passare dalla
linea rossa a quella verde.
Ecco il nuovo metodo di test:
public
void testSequenzaSingola(){
assertTrue(Sequenza.sequenzaSingola(10)
== 6);
assertTrue(Sequenza.sequenzaSingola(12)
== 9);
}
Ed
ecco l'implementazione in Sequenza che permette il funzionamento
corretto del test.
static
int sequenzaSingola(int start){
int val = start, count = 0;
while(val>1){
val=modifica(val);
count++;
}
return count;
}
Ora,
se torniamo alla definizione del problema, ci accorgiamo
che il valore di cui cerchiamo la lunghezza della sequenza
dev'essere positivo e maggiore di 1. Questo significa
che nell'implementazione della soluzione dobbiamo prevedere
una verifica e un'azione da eseguire nel caso in cui
il controllo desse risultato negativo.
Vedremo questo nella seconda parte, pubblicata il mese
prossimo.
Conclusione
Il test automatico come elemento di progettazione e
sviluppo viene sempre più utilizzato in modo
sistematico. Abbiamo cercato di analizzare come mai
questo utilizzo fatichi ancora ad ottenere riscontro
nei corsi di programmazione.
JUnit è senz'altro uno degli elementi principali
che permette di avvicinare gli sviluppatori Java alla
metodologia del test di unità. Il test di unità
è la base essenziale per l'evoluzione del software,
perché aumenta la fiducia dello sviluppatore
nel proprio codice, e quindi anche nella sua capacità
di modificarlo.
Abbiamo iniziato un esempio che termineremo il mese
prossimo. Nel prossimo articolo tratteremo inoltre il
tema del test per siti Web, introducendo tools come
HttpUnit [6] e CanooWebTest [7].
Bibliografia
[1]
JUnit: http://www.junit.org
[2] Fowler Martin: Refactoring, Improving the Design
of Existing Code, Addison Wesley, 1999.
[3] Pedrazzini Sandro: Frameworks e Patterns: A Caccia
di Patterns, Moka Byte, http://www.mokabyte.com, Marzo
2001.
[4] Gamma E., Helm R.Johnson R., Vlissides J.: Design
Patterns, Elements of Reusable Object-Oriented Software,
Addison Wesley, 1995.
[5] http://www.extreme-programming.org
[6] HttpUnit: http://httpunit.sourceforge.net
[7] CanooWebTest: http://webtest.canoo.com
|