MokaByte 86 - Giugno 2004 
Pratiche di sviluppo del software
I parte: Test Driven Development

di
Stefano Rossini

La letteratura relativa allo sviluppo di software è ricca di pattern e best practices; nei fatti non sempre quanto formalizzato in letteratura trova una corretta e sistematica applicazione nelle pratiche di sviluppo. All'atto pratico, alcuni importanti temi spesso non sono inclusi in modo sistematico all'interno dei processi di sviluppo. Uno di questi temi, forse il più importante, è quello dei Test. In questo articolo affronteremo un'importante pratica per lo sviluppo del software: il Test Driven Development (TDD).

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:

  1. non scrivere codice applicativo se non si dispone di un test automatico che fallisce
  2. 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:

  1. aggiungere un test prima di codificare la funzionalità applicativa
  2. codificare la funzionalità applicativa in una forma minimale purchè compilabile
  3. lanciare il test (che dovrà fallire)
  4. modificare il codice applicativo che implementa la relativa funzionalità
  5. eseguire nuovamente il test
  6. se il test fallisce, bisogna provvedere alle relative modifiche del codice applicativo (passo 4) e quindi rieseguire i test (passo 5).
  7. se il test ha successo
    1. provvedere a "ripulire" il codice da eventuali duplicazioni o statement non strettamente necessari e rieseguire il test (passo 5)
    2. 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


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