MokaByte 63 - Maggio 2002 
Con il test in testa
I parte
di
Sandro Pedrazzini
L'elemento di sviluppo software di cui fino a qualche tempo fa meno si parlava non solo nei corsi universitari, ma anche tra sviluppatori, era senza dubbio il test. Nei corsi veniva spesso trattato come un passaggio obbligatorio ma banale di cui non valeva la pena parlare, se non in termini molto astratti. Ognuno avrebbe poi imparato nella pratica come fare... Il test automatico come elemento di progettazione e sviluppo viene però ora sempre più utilizzato in modo sistematico. Le cose stanno quindi cambiando, e non è un caso che per gli sviluppatori Java questo corrisponda con il diffondersi di un certo numero di tool di test come JUnit.

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

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it