Sei sulla pagina 1di 28

Capitolo 2: Test-Driven

Development
Introduzione
Circa una quindicina di anni fa, un informatico statunitense, Kent Beck, ideò
un nuovo processo di sviluppo software: il Test-Driven Development (TDD).
Quest’ultimo trae le sue radici dalle tecniche agili e, in particolare, dalla
metodologia di Extreme Programming. TDD è uno stile di sviluppo, di
progettazione, non è una tecnica di test. Secondo Beck, infatti, era
necessario ridurre la distanza tra le decisioni che vengono prese nello
sviluppo del software ed il feedback che il committente rilascia.

Il principale obiettivo del Test-Driven Development è sviluppare codice


“pulito” e funzionante. Fino a qualche tempo fa tale obiettivo era difficile da
raggiungere anche dai migliori programmatori. Essi, infatti, procedevano
inizialmente con la stesura del codice funzionante e, solo successivamente,
lo rendevano pulito. Con l’affermarsi del TDD, invece, ci fu un’inversione di
tendenza: prima, infatti, occorre rendere pulito il codice e, solo in seguito,
renderlo funzionante. In altre parole, occorre:

 Rendere lo sviluppo migliore e più rapido;


 Mantenere il codice sempre documentato;
 Ottenere codice flessibile e facilmente estendibile;
 Diminuire la presenza di bug nel codice di produzione.

Il Test-Driven Development, dunque, ruota attorno a due concetti


fondamentali:

 TEST FIRST PROGRAMMING, ovvero la classe di test deve essere


creata antecedentemente al codice di produzione;
 REFACTORING, ovvero dopo la creazione del test e del codice di
produzione è opportuno effettuare il “refactoring”.
Il TDD si articola nelle seguenti fasi:

1) Scrivere un test
2) Eseguire il test e verificare che fallisce
3) Scrivere il codice di produzione
4) Eseguire nuovamente il test
5) Eseguire il “refactor” del codice

Inizialmente, dunque, occorre creare una nuova classe di test che,


inevitabilmente, dovrà fallire poiché ancora non è stato scritto il codice di
produzione. Naturalmente, il test dovrà essere creato in base alle specifiche
ed ai requisiti che vogliamo ottenere dal nostro software. Quest’ultimo aspetto
può sembrare banale, ma è di fondamentale importanza.

Eseguiamo, quindi, tale test e verifichiamo che fallisca. Se ciò accade, allora
possiamo dire che il test non è inutile, ovvero non passa sempre. Invece, se il
test non fallisce, allora c’è un problema ed occorre analizzare adeguatamente
la classe di test.

Se il test fallisce, allora possiamo scrivere il codice di produzione, ovvero il


codice che vogliamo testare. Tale classe deve essere creata con le sole
funzionalità che permettano al test di passare; nessuna funzionalità
aggiuntiva deve essere inserita nella classe di test.
Dopo aver sviluppato adeguatamente il codice, possiamo eseguire
nuovamente il test che avevamo creato precedentemente. Se il test passa,
allora siamo sicuri di aver prodotto codice funzionante.

Infine, dopo aver verificato la correttezza del codice, quest’ultimo deve essere
opportunamente modificato per eliminare, ad esempio, eventuali problemi di
duplicazione. Tale procedimento viene anche denominato “refactoring”.
Naturalmente, dopo aver modificato adeguatamente tale codice, occorre
ripetere il test e verificare se funziona anche dopo aver effettuato il
refactoring.

Tale argomento può essere approfondito nel libro “Test-Driven Development


by example” scritto da Kent Beck.

Possiamo dunque distinguere tre regole fondamentali del Test-Driven


Development, come sottolineato anche da Robert Martin nel suo libro “Clean
Code”:

I. Non puoi scrivere codice di produzione se prima non hai scritto una
classe di test che fallisca;
II. Non puoi scrivere più di una classe di test che fallisca;
III. Non puoi scrivere più codice di produzione di quello sufficiente a far sì
che il test, che precedentemente ha fallito, possa funzionare.

È possibile, inoltre, identificare un “mantra” del Test-Driven Development,


ovvero:

“Test code is just important as production code”


che significa

“Il codice della classe di test è tanto importante quanto il codice da testare”

A chi è rivolto il Test-Driven Development?


Come sottolineato anche da Kent Beck nel libro “Test Driven Development by
example”, a volte l’obiettivo del programmatore è solo scrivere codice
funzionante, senza preoccuparsi di eventuali migliorie che potrebbe apportare
successivamente al software. Il Test-Driven Development non è rivolto a tali
persone, ma piuttosto a coloro che vogliono incrementare sempre di più la
qualità del software e che non si fermano, dunque, al primo successo.
Il TDD, infatti, aiuta i programmatori a prestare attenzione alle questioni
dubbiose, non ancora del tutto chiarite, nel momento in cui sono
fondamentali, rendendo così possibile al programmatore di perfezionare il
software prodotto. Tale metodologia consente, dunque, di scrivere codice con
un minor numero di difetti ed avere un design più pulito. Di conseguenza,
possiamo notare come il Test Driven Development aumenti anche la fiducia
del programmatore nei propri mezzi. Se, infatti, inizialmente egli era scettico
delle proprie qualità ed aveva avuto alcune difficoltà a sviluppare il software,
ora con l’utilizzo del TDD diventa più semplice sviluppare un buon software.

Strategie del Test-Driven Development


Per fare in modo che il test funzioni possiamo distinguere 3 strategie:

 Implementazione fasulla

 Triangolazione

 Implementazione diretta

Per quanto riguarda l’implementazione fasulla, essa prevede l’inserimento nel


test di una costante che, gradualmente, viene sostituita con alcune variabili al
fine di ottenere il codice corretto e funzionante.

Un’altra strategia possibile è nota come “Triangolazione”. L’idea di base


consiste nel creare un nuovo test, o modificare quello esistente, al fine di
verificare l’inesattezza o meno dell’implementazione attuale. Essa viene
utilizzata soprattutto quando sorgono dubbi riguardo alla realizzazione finale.

Esiste, infine, un’ultima strategia: l’“implementazione diretta”. Rispetto alle


precedenti, essa è molto difficile da attuare poiché richiede la perfezione del
codice prodotto. Il principale problema dell’implementazione diretta, quindi,
risiede nel fatto che è difficile produrre codice pulito e perfettamente
funzionante allo stesso tempo.

Vantaggi del Test-Driven Development


Sebbene nessuno studio abbia finora dimostrato la differenza, in termini di
qualità e produttività, tra il Test Driven Development e le sue molteplici
alternative, possiamo facilmente dire che il TDD apporta innumerevoli
vantaggi a coloro che si avvalgono del suo utilizzo, sia programmatori che
clienti. Così facendo, infatti, si evita al programmatore di dover risolvere tutto
in una volta e, al contempo, si garantisce al cliente un valido strumento per
poter interagire allo sviluppo del prodotto software.

Il TDD permette, dunque, al programmatore di concentrarsi esclusivamente


sull’esecuzione di un determinato test e, solo successivamente,
sull’esecuzione dei test rimanenti. Egli, infatti, può dedicarsi allo sviluppo di
piccoli pezzi di codice che saranno subito testati e, quindi, può procedere
“step by step”. Utilizzando tale metodologia, inoltre, egli diminuisce anche lo
stress e la pressione che lo affliggono e, di conseguenza, migliora anche il
rapporto tra i vari programmatori all’interno del team di sviluppo del software.

Un altro vantaggio del Test Driven Development risiede nella possibilità di


accorciare il tempo che intercorre tra lo sviluppo di una particolare
funzionalità del software e la rispettiva valutazione del cliente. Prima
dell’avvento del TDD, infatti, occorreva aspettare settimane o mesi prima di
conoscere la reazione del cliente riguardo al software prodotto o, più
semplicemente, riguardo ad una specifica funzionalità di esso. Ora, invece,
sono sufficienti alcuni minuti o secondi per avere il feedback del cliente.

Altri vantaggi che possono derivare dall’utilizzo di tale metodologia possono


essere individuati, ad esempio, nella facile comprensione del codice, nel
rapido sviluppo dello stesso e nella possibilità di trovare eventuali bug nella
fase iniziale dello sviluppo software e non al termine di essa, come invece
accadeva precedentemente.

Test
Che cosa si intende, però, per test?

Un test è un insieme di operazioni che vengono effettuate al fine di verificare se una determinata
funzionalità è soddisfatta. Tuttavia, quando ci si riferisce alla classe di test è possibile fare
confusione; esistono, infatti, due tipi di test: test di unità e test funzionali. Per test di unità si
intende il test dal punto di vista del programmatore, ovvero la classe che mi permette di verificare
se il codice che ho appena prodotto risponde adeguatamente alle mie richieste. Quando si parla di
test funzionali, invece, ci si riferisce ai test dal punto di vista dell’utente finale. In altre parole,
questi ultimi sono test in cui è necessario introdurre un input ed il programma si occupa di
restituire l’output, senza però mostrare i calcoli che ha effettuato per ottenere tale risultato.

Il nostro obiettivo è avere codice pulito e funzionante. Per far ciò occorre che,
a maggior ragione, anche la classe di test sia pulita, ossia leggibile. Occorre,
infatti, che tale classe sia chiara, semplice e a bassa densità di espressione.
In un test si vuole dire molto con il minor numero di espressioni possibili.

Come scritto anche nel libro “Clean Code”, di Robert Martin, possiamo
distinguere 5 regole fondamentali che permettono di avere un test pulito
(F.I.R.S.T.):

 FAST: ossia i test devono essere veloci. Se così non è, allora si tende
ad eseguirli il meno possibile, provocando enormi problemi al codice di
produzione. Se i test non vengono eseguiti frequentemente, infatti,
eventuali problemi non verranno rilevati abbastanza presto per essere
risolti con facilità.

 INDEPENT: i test, infatti, non devono dipendere l’uno dall’altro, bensì


devono essere indipendenti. Se così non fosse, il fallimento di un test
provocherebbe enormi danni poiché la diagnosi sarebbe difficile ed i
difetti sarebbero facilmente rilevabili.

 REPEATABLE: i test dovrebbero essere ripetuti in qualsiasi ambiente.


Se ciò non è possibile, il programmatore avrà sempre una scusa per cui
i test non riescono.

 SELF-VALIDATING: i test dovrebbero avere un’uscita booleana; o si


passano o si falliscono. Non si dovrebbe leggere un file di log per
capire se i test passano o falliscono, ma bensì potremo capirlo
dall’uscita booleana. In questo modo, eviteremo di confrontare
manualmente i due file di testo per vedere se i test passano.

 TIMELY: i test devono essere scritti in modo tempestivo. Essi, infatti,


dovrebbero essere scritti antecedentemente al codice da testare. Se ciò
non avviene, ovvero se il test viene scritto dopo il codice di produzione,
allora tale codice potrebbe essere difficile da testare. Non è possibile
progettare codice di produzione testabile.

Come sostiene anche Sandro Pedrazzini nel libro “Elementi di progettazione


agile: utilizzo pratico di design pattern, refactoring e test” il test di unità è
garanzia di affidabilità del programma e, soprattutto, di fiducia nel codice da
parte del programmatore. Il testing, tuttavia, spesso viene tralasciato dai
programmatori. Essi, infatti, vengono spesso condizionati dalle tempistiche a
cui devono attenersi, dall’eccessiva sicurezza nel codice che hanno
sviluppato e dalla loro scarsa professionalità. Sebbene, infatti, al giorno
d’oggi le tempistiche siano sempre più strette, essi dovrebbero evitare di
trascurare l’attività di test. Se ciò non accadesse, infatti, diminuirebbe la
stabilità del codice e gli errori comparirebbero a cascata. Altri aspetti da non
sottovalutare sono l’eccessiva sicurezza e la scarsa professionalità del
programmatore. Egli, infatti, deve rendersi conto che è impossibile produrre
immediatamente codice senza errori, soprattutto se si evita di eseguire
radicalmente l’attività di test. È possibile, tuttavia, ridurre al minimo tale
possibilità di errori utilizzando i test di unità in modo sistematico, ovvero
automaticamente dal programma.

I principali benefici che si ottengono sono i seguenti:

o Accorciamento dei cicli di sviluppo del codice. In altre parole, viene


diminuito il tempo che intercorre tra i vari cambiamenti del codice,
senza che il software si trovi in uno stato inconsistente.

o Aumenta l’autostima nel programmatore e nel software prodotto

o Aumenta la predisposizione del programmatore al cambiamento del


codice, anche se funzionante
o Facilita il refactoring del codice

o Aumenta la produttività. Il test automatico, infatti, richiede un costo


iniziale che, però, viene ammortizzato ogni volta che tale test viene
eseguito automaticamente dal programma.

JUnit
JUnit è uno dei più importanti Java frameworks.

Che cos’è un framework?

Un framework è una struttura logica su cui un software può essere progettato e realizzato,
facilitandone lo sviluppo. Naturalmente, alla base di un framework ci sono molte librerie di codice
utilizzabili con uno o più linguaggi di programmazione. Esse, inoltre, sono spesso corredate da
strumenti di supporto allo sviluppo del software che hanno lo scopo di aumentare la velocità di
sviluppo del prodotto finito. L’utilizzo di un framework, dunque, impone al programmatore anche
una precisa metodologia di sviluppo del software.

A cosa serve un framework?

Lo scopo di un framework è risparmiare allo sviluppatore la riscrittura del codice già scritto in
precedenza per compiti similari. L’utilizzo di un framework, dunque, permette al programmatore
di non farsi carico della scrittura del “codice di contorno”, ma solo focalizzarsi sul contenuto vero
e proprio dell’applicazione.
Come è facile intuirne dal nome, infatti, JUnit è un framework scritto in Java
ed è un’istanza della classe xUnit.

Cos’è xUnit?

Con il termine xUnit si intendono tutti quei framework, come ad esempio JUnit, che compongono
la famiglia dei code-driven testing framework. I vantaggi principali che le varie istanze della classe
xUnit forniscono sono essenzialmente due, ovvero:

 possibilità di non scrivere gli stessi test più volte;

 nessuna necessità di ricordare il risultato atteso da parte dei test.

La prima implementazione della classe xUnit fu denominata SUnit. Tale framework si basa sul
linguaggio di programmazione SmallTalk e permette, dunque, ai programmatori di scrivere test e
verificarli. Il principale problema di SUnit, tuttavia, è SmallTalk. I programmatori, infatti, devono
essere in grado di scrivere semplici programmi se utilizzano SmallTalk.

Solo successivamente, Erich Gamma e Kent Beck portarono SUnit al linguaggio di programmazione
Java, creando così JUnit. Dopo JUnit, naturalmente, furono molteplici i framework che vennero
creati sulla base dei diversi linguaggi di programmazione come, ad esempio, CppUnit, NUnit e
MATLAB Unit Testing Framework.

Junit, in particolare, è stato realizzato da Kent Beck ed Erich Gamma per


facilitare lo sviluppo e l’organizzazione dei test di unità. Tale framework può
essere utilizzato a parte oppure integrato nel proprio ambiente di sviluppo,
come, ad esempio, Eclipse. Beck e Gamma, infatti, hanno anche pubblicato
un libro denominato “Contributing to Eclipse” dove spiegano come realizzare
un plug in che permetta l’integrazione di Junit con la piattaforma di sviluppo
Eclipse. Essi, però, analizzano anche gli scopi di Junit e il contributo che tale
software fornisce agli sviluppatori nell’attività di testing:

 Scrivere test deve essere semplice;

 I test devono essere automatici;


 I test devono essere componibili, ovvero Junit deve mettere a
disposizione la possibilità di combinare diverse sequenze di test (o
suite);

 I singoli test devono essere isolati; infatti, il singolo successo o


insuccesso di un test non deve influenzare il risultato degli altri test.

Come possiamo notare dalla foto sottostante, l’interfaccia grafica di Junit è


molto semplice, ma anche molto efficace. Infatti, se il risultato è quello atteso,
allora Junit mostra una barra verde, altrimenti si è verificato un problema e,
dunque, Junit mostra una barra rossa. I possibili problemi che si possono
presentare sono essenzialmente due: error e failure.

Esempio di Junit su Eclipse

ASSERT
Come sostiene anche Sandro Pedrazzini nel libro “Elementi di
programmazione agile: utilizzo pratico di design pattern, refactoring e test” il
meccanismo di assert è il principio che sta alla base di JUnit.

Assert non è altro che un predicato che restituisce vero o falso. In particolare,
egli si preoccupa di confrontare il risultato ottenuto con il risultato atteso. Se
coincidono, allora restituisce vero, altrimenti restituisce falso. I metodi
assert(), dunque, permettono di mantenere più semplice il codice, senza la
necessità di realizzare il confronto in modo esplicito.

AssertTrue() è il più importante tra i vari tipi di assert poiché è il più generico
e permette di verificare in modo esplicito un’intera espressione booleana.

Tuttavia, esistono molteplici tipi di assert, ognuno con un compito specifico:

 assertTrue()

 assertEquals()

 assertNull()

 assertNotNull()

 assertFalse()

 assertSame()

 assertNotSame()

ERROR E FAILURE
Come dicevamo precedentemente, i possibili problemi che provocano un
fallimento nell’esecuzione dei test e, conseguentemente, la comparsa della
barra rossa in JUnit sono essenzialmente due: error e failure.

Un failure viene rilevato laddove una o più asserzioni all’interno di un test


risulti falsa, mentre si ha un errore se si è verificata un’eccezione imprevista
durante il test. In quest’ultimo caso, dunque, JUnit riporta il verificarsi di
un’eccezione non gestita. Naturalmente, non può essere considerato un
fallimento in quanto si tratta di un’uguaglianza non verificata e perciò viene
classificata come errore e non come failure.

ALTRI METODI
Oltre ai metodi assert(), infatti, all’interno di un test è possibile utilizzare
anche metodi come setUp() e tearDown(). Entrambi vengono ereditati come
metodi vuoti e, dunque, sovrascrivendoli si crea una fixture utilizzabile in tutti i
test.

Il metodo setUp() viene chiamato prima di ogni metodo di test ed ha lo scopo


di centralizzare l’inizializzazione degli elementi comuni avendo così metodi di
test più corti e codice più sintetico. Inoltre, eviteremo lo svantaggio di dover
dichiarare le variabili globali con gli effetti collaterali che ciò comporterebbe.
Ciò non significa automaticamente che, utilizzando setUp(), aumentiamo
anche la chiarezza del codice. Spesso, infatti, come sostiene anche
Pedrazzini in “Elementi di progettazione agile: utilizzo pratico di design
pattern, refactoring e test”, le ridondanze permettono di capire meglio ogni
singolo metodo.

Il metodo tearDown(), invece, viene utilizzato dopo l’esecuzione di ogni


metodo di test. Tale metodo è molto utile quando si utilizzano risorse esterne
come, ad esempio: file, connessioni a un database, …. Esse, infatti, devono
essere rilasciate dopo l’utilizzo di ogni singolo test.

TEST DOUBLE
Spesso succede che, per far funzionare adeguatamente il nostro software,
abbiamo bisogno di elementi esterni come, ad esempio, l’accesso a un
database o ad un server in rete. Se vogliamo solamente verificare la
funzionalità del codice, senza interagire con l’esterno, è possibile utilizzare
oggetti simulati che imitano il comportamento degli oggetti reali. Come
sostiene anche Gerard Meszaros nel libro “xUnit Test Patterns: Refactoring
Test Code” il termine generico per indicare tali oggetti sostitutivi è Test
Double. In particolare, Meszaros definisce anche quattro tipi particolari di
Test Double:
 Dummy Objects: gli oggetti dummy sono oggetti “segnaposto”, ossia
oggetti che vengono passati come argomento ma mai realmente
utilizzati. In genere, infatti, vengono utilizzati per riempire la lista di
parametri.

 Fake Objects: tali oggetti hanno implementazioni funzionanti, ma di


solito non sono particolarmente adatti per ambienti di produzione
perché utilizzano delle “scorciatoie”.

 Stubs: gli stubs forniscono risposte pre-costruite a chiamate effettuate


durante il test, senza rispondere a nulla che sia al di fuori di ciò che è
previsto per il test.

 Mock Objects: sono oggetti pre-programmati che permettono, però, al


programmatore di specificare anche quali metodi saranno richiamati
sull’oggetto fittizio, quali parametri gli saranno passati come argomento
e quali valori verranno restituiti.

Tuttavia, solo utilizzando i mock objects è possibile verificare il


comportamento dell’oggetto, oltre alla verifica di stato dello stesso.

Dummy Objects

Come dicevamo precedentemente, gli oggetti dummy sono oggetti


“segnaposto”, ossia oggetti che vengono passati come argomento da metodo
a metodo, ma mai realmente utilizzati. Se necessitiamo di un oggetto dummy
occorre, innanzitutto, creare un’istanza di un oggetto che può essere
inizializzato in modo facile e senza dipendenze. Dopodiché possiamo
passare tale istanza come argomento di un metodo del sistema in
esecuzione. Poiché tale istanza non verrà effettivamente utilizzata dal
sistema in esecuzione, allora non abbiamo bisogno di nessuna
implementazione per tale oggetto. Se ciò non avviene, allora il sistema
genera un errore poiché cerca di invocare un metodo inesistente. L’utilizzo
dei dummy objects, dunque, evita al programmatore di scrivere codice
irrilevante che, invece, sarebbe stato necessario per creare gli oggetti reali.

Fake Objects
I Fake Objects sono oggetti fittizzi che hanno implementazioni funzionanti.
Tuttavia, rispetto all’oggetto reale che rappresentano, sono molto più semplici
e leggeri poiché non necessitano di molte proprietà che, invece, l’oggetto
reale deve possedere come, ad esempio, la scalabilità. Un Fake Object deve
fornire solo i servizi di cui necessita il sistema in esecuzione facendo in
modo, quindi, che quest’ultimo rimanga inconsapevole sul tipo di oggetto che
sta utilizzando, se reale o fittizio.

Stubs

Gli stubs sono oggetti fittizi che simulano il comportamento dell’oggetto reale
corrispondente senza però fornire alcuna risposta su ciò che non è previsto
dal test. L’utilizzo di uno stub, dunque, permette al programmatore di testare
parti di codice del test che, altrimenti, non sarebbe stato possibile eseguire.
Tuttavia, il loro utilizzo, dal punto di vista del programmatore, è piuttosto
limitato poiché forniscono risposte pre-costruite alle chiamate fatte durante il
test.

Mock Objects

Similmente agli stubs, anche i mock objects sono oggetti fittizi che simulano il
comportamento del corrispondente oggetto reale. Rispetto agli stubs, tuttavia,
i mock objects confrontano le chiamate effettivamente ricevute durante il test
con le chiamate che ci si aspettava venissero effettuate e che sono state
precedentemente definite mediante opportune asserzioni.

Vi sono due tipi di Mock Objects:

 Mock objects strict, che falliscono il test se le chiamate che vengono


ricevute sono in un ordine differente da ciò che ci si aspettava;

 Mock objects lenient, che tollerano anche le chiamate non previste.


Essi, infatti, tollerano o addirittura ignorano le chiamate inaspettate o
perse.

Esempi di JUnit
1) CONTO CORRENTE
Implementare tutte le possibili operazioni che un cliente può effettuare sul
proprio conto corrente relativo ad una determinata banca X.

Le principali operazioni che il cliente può eseguire sono:

 Prelievo: il cliente ritira soldi dal proprio conto corrente;

 Deposito: il cliente deposita soldi nel proprio conto corrente;

 Saldo: il cliente richiede il saldo del proprio conto corrente.

Svolgimento

Inizialmente, abbiamo dunque creato la classe di test che, necessariamente,


dovrà fallire perché ancora non abbiamo creato il codice di produzione.

Verifichiamo ora che il test fallisca.


Il test ha fallito e, dunque, possiamo dire che non è inutile, ovvero non passa
sempre. Verificato ciò, ora possiamo scrivere il codice di produzione della
classe BankAccount. Tale classe, però, deve contenere solo i metodi e gli
attributi che permettano al test di passare; non deve contenere, infatti,
nessuna funzionalità aggiuntiva.

Dopo aver sviluppato il codice, eseguiamo il test.


Il test passa e, quindi, siamo sicuri di aver prodotto codice funzionante. Per
quanto riguarda il refactoring del codice, è stato solo aggiunto il costruttore
con settaggio a zero del saldo quando si crea un nuovo conto ed è stato
creato anche un controllo sulla somma da prelevare, per fare in modo che
non sia negativo e che non ecceda il saldo nel conto.

Verifichiamo, quindi, che il test funzioni ancora.


La barra è verde e non sono stati rilevati né errori né failure, quindi il test
funziona ancora.

2) MONETE
In questo caso, occorre implementare la somma e la sottrazione tra monete
di diverso tipo. Se vogliamo fare la somma tra due monete dello stesso tipo,
allora si tratterà di una semplice somma numerica. Altrimenti, se le due
monete sono di valute diverse, vogliamo che non sia effettuata
immediatamente la conversione delle monete perché il tasso di cambio
potrebbe variare nel tempo. Occorre, dunque, gestire opportunamente tale
somma.

Svolgimento

Creiamo, dunque, la classe di test, ovvero MoneteTest. Le altre classi sono


state create, con i relativi metodi, ma sono completamente vuote.
Inizialmente, infatti, occorre verificare che il test non passa mai.
Nel test soprastante abbiamo implementato sia la somma di due monete
della stessa valuta, ovvero l’euro, sia la gestione di monete di diverso tipo
come il dollaro, lo yen e l’euro. Con gli assert, inoltre, abbiamo verificato che:

 la somma delle due monete di euro sia avvenuta con successo;

 la somma delle monete di euro nel salvadanaio sia stata effettuata con

successo.

Dopo aver implementato opportunamente la classe di test, ora verifichiamo


che fallisca.
Come possiamo notare il test ha fallito. Ora, dunque, possiamo scrivere il
codice di produzione che permetterà al test di passare.

La classe Moneta è la seguente:


La classe Salvadanaio, ovvero la classe che può contenere monete di
diverso tipo, è stata, invece, implementata nel seguente modo:
Ora possiamo rieseguire il test e verificare che passi, ovvero che la barra
diventi verde e che non siano presenti né failure né errori.

Per quanto riguarda il refactoring, non è necessario perché non sono state
rilevate eventuali duplicazioni e/o migliorie che possono essere effettuate,
senza aggiungere alcune funzionalità extra al software.

Cosa non si deve testare?


Secondo Kent Beck in “Test Driven Development by example”, dobbiamo
scrivere test “finchè la paura non si trasforma in noia”. Egli sostiene, infatti,
che spetta al programmatore decidere cosa testare e cosa non testare.
Possiamo dire, però, che le classi di test sono fondamentali laddove sono
presenti nel codice alcune istruzioni condizionali, dei cicli, quando si
effettuano operazioni all’interno del nostro codice o, infine, quando vogliamo
verificare il polimorfismo. I test, tuttavia, devono essere eseguiti solo se il
programmatore che li ha creati è lo stesso che ha provveduto alla sviluppo
del codice sul quale si vogliono eseguire i suddetti test. Se, infatti, il codice è
stato realizzato esternamente allora non c’è motivo per cui il team di sviluppo
abbiamo motivo di diffidare della sua corretta realizzazione.
Come riconoscere la bontà di un test?
Quando si creano le classi di test occorre verificare la bontà del test appena
creato. Dobbiamo, infatti, riconoscere se il test è buono oppure no. Secondo
Kent Beck, per fare ciò, è possibile analizzare alcuni elementi fondamentali,
tra cui:

 la fragilità del test;

 la velocità del test;

 la lunghezza del codice di testing.

Un elemento che ci permette di verificare la bontà o meno di un test è,


appunto, la lunghezza del codice della classe di test. Se, infatti, utilizziamo
molte linee di codice per creare nuovi oggetti che servono semplicemente per
un semplice assert, allora qualcosa non torna nel codice. Tali oggetti, infatti,
sono troppo grandi e devono essere adeguatamente suddivisi per far sì che il
test sia buono.

Occorre, inoltre, verificare anche la fragilità dei test. Alcune volte, infatti,
succede che i test si fermano inaspettatamente durante la loro esecuzione
poiché una parte del software è stata influenzata da un’altra parte dello
stesso. Occorre, dunque, interrompere tale “connessione” oppure far in modo
di riunire le due parti.

Un altro elemento che occorre analizzare attentamente è la velocità del test,


ovvero il tempo che occorre al test per terminare normalmente la propria
esecuzione. Se, infatti, un test è lento, ovvero impiega molto tempo per
terminare la propria esecuzione, allora il programmatore o i membri del team
di sviluppo del software tenderanno ad eseguirlo il meno possibile o,
addirittura, a non eseguirlo proprio. Così facendo, tuttavia, compromettiamo
lo sviluppo del suddetto software che stiamo producendo e della classe di
test. Occorre, dunque, cercare di aumentare la velocità di esecuzione dei test
per far, così, incrementare il numero di esecuzioni dello stesso.
Quando si dovrebbe eliminare una classe di test?
Un’ulteriore domanda che i programmatori si dovrebbero porre quando
procedono allo sviluppo di software è quando eliminare una classe di test.
Tale scelta dipende essenzialmente da due fattori: la fiducia e la
comunicazione.

Non bisogna mai, infatti, eliminare un test se tale eliminazione riduce la


confidenza del programmatore nei confronti del sistema. Inoltre, se i due test
effettuano le stesse operazioni, ma per il cliente sono fondamentali entrambe
perché, ad esempio, rappresentano scenari diversi, allora occorre che il
programmatore mantenga entrambe le classi. Possiamo però dire che, se
disponiamo di due test ridondanti rispetto alla fiducia ed alla comunicazione,
ovvero se l’eliminazione del suddetto test non riduce la fiducia del
programmatore e non complica la comunicazione del cliente, allora è
possibile eliminare il meno utile dei due test.

Come valutare la qualità di un test?


Come sostenevamo anche precedentemente, occorre anche poter valutare la
qualità di un test. Come sostiene anche Kent Beck nel suo libro “Test-Driven
Development by example”, è possibile fare ciò attraverso due metodi:
Statement Coverage e Defect Insertion.

STATEMENT COVERAGE
Utilizzando il metodo denominato “Statement Coverage” è possibile valutare
la qualità di un test semplicemente analizzando il numero di istruzioni
eseguite ed il numero di istruzioni che, eventualmente, non sono state
eseguite a causa di un blocco nel codice.

La formula è la seguente:
VANTAGGI:

 Consente di verificare che cosa ci si aspetti che il software faccia e non


faccia;

 Si misura la qualità del codice scritto.

SVANTAGGI:

 Non può testare le false condizioni;

 Non segnala se il ciclo ha raggiunto la condizione di terminazione;

 Non supporta gli operatori logici;

DEFECT INSERTION
Un altro metodo per valutare la qualità di un test è il “Defect Insertion”. L’idea
fondamentale di tale metodo consiste nel cambiare il significato di una riga di
codice ed analizzare il comportamento del test. Se il test si ferma, allora la
qualità del test è buona, altrimenti occorre modificarlo per aumentarne la
qualità.

Naturalmente, è possibile fare ciò manualmente o tramite apposito software


(es.: Jester).

ATDD: Acceptance Test Driven Development


Come abbiamo già abbondantemente annunciato, TDD (Test Driven
Development) è uno strumento di sviluppo che aiuta il programmatore a
scrivere codice in grado di eseguire correttamente una serie di operazioni.

Per ATDD (ovvero Acceptance Test Driven Development) intendiamo invece


uno strumento di comunicazione tra il cliente, il programmatore ed il tester
per garantire che i requisiti del software in via di sviluppo siano ben definiti.

L’Acceptance Test Driven Development, dunque, non riguarda la parte di


testing, ma soprattutto fa riferimento alla comunicazione, alla collaborazione
ed alla chiarezza che i programmatori devono avere con il cliente. Spesso,
infatti, i requisiti che un software deve avere vengono fraintesi tra il tester ed il
cliente. Grazie alle metodologie agili, si cerca di ridurre al minimo tali
fraintendimenti lavorando sul codice a piccoli passi e cercando di confrontarsi
con il cliente. Quest’ultimo, infatti, deve opportunamente discutere su ciò che
è necessario o non è necessario che il software possieda. La chiave di ogni
progetto software di successo è, infatti, creare un prodotto che soddisfi un
bisogno reale. Solo una conoscenza approfondita di tale bisogno assicura,
dunque, che la funzionalità del software abbia una ripercussione sulla vita
reale, aiutando così le aziende a raggiungere i loro obiettivi. Per questo
motivo, tutti coloro che collaborano alla creazione del software devono
conoscere il perché sono necessarie alcune funzionalità del software e,
soprattutto, come il software dovrebbe funzionare.

Grazie all’ATDD, dunque, i clienti possono utilizzare un meccanismo


automatico al fine di decidere se il software prodotto dal team di sviluppo
soddisfi o meno le loro esigenze. I programmatori, dunque, ora hanno anche
l’obiettivo specifico di soddisfare i vari test di accettazione, ovvero finestre di
dialogo tra il cliente ed il team di sviluppo dove è possibile definire e chiarire
meglio i requisiti software del prodotto che si vuole creare.

Come funziona ATDD?


ATDD, concettualmente, è molto semplice: prima il cliente, gli sviluppatori ed i
tester discutono dei requisiti che il software deve avere e, solo
successivamente, vengono effettuate domande e risposte al fine di
comprendere meglio il software che il cliente desidera. Le risposte che il
cliente fornisce devono essere comunicate sotto forma di un test di
accettazione, come già annunciato precedentemente. Naturalmente, tali test
di accettazione vengono forniti e ricevuti anche se non si usa il metodo di
Acceptance Test Driven Development. Tuttavia, il grande cambiamento di
ATDD consiste nel ricevere i test di accettazione dal cliente prima che il
codice sia sviluppato, evitando così inutili incomprensioni che
provocherebbero un’enorme mole di lavoro supplementare per poter risolvere
tali problemi.

Come sostenuto anche nel sito www.netobjectives.com, quando un team di


sviluppo decide di utilizzare l’Acceptance Test Driven Development, occorre
anche stabilire a quale livello utilizzare tale metodologia. Distinguiamo, infatti,
quattro possibili livelli di ATDD. Il primo ed il secondo livello prevedono poco
o addirittura nessun lavoro extra, mentre il terzo ed il quarto livello richiedono
un investimento maggiore, ma restituiscono un valore migliore. È possibile
iniziare a qualsiasi livello e, come abbiamo detto precedentemente, ogni
livello richiede più investimenti, ma fornisce anche un migliore valore di
ritorno.

LIVELLO 1: Utilizzare le specifiche del test come strumento di analisi e


convalida dei requisiti

In questo caso, gli sviluppatori utilizzano i test di accettazione per cercare di


comprendere meglio ciò che non conoscono. Essi, infatti, devono essere
sicuri di aver ben definito tutti i criteri di accettazione prima di scrivere il
codice. In questo livello, quindi, non è previsto lavoro supplementare rispetto
al solito poiché tutto quello che i membri del team di sviluppo devono
effettuare non è altro che sviluppare i test prima di scrivere il codice. Si tratta,
dunque, di una riorganizzazione dei flussi di lavoro.

LIVELLO 2: Analisti di business proprietari, sviluppatori e tester


parlano simultaneamente

Uno dei principali vantaggi dell’Acceptance Test Driven Development è che


cambia la natura della conversazione tra cliente, sviluppatori e tester. Questo,
dunque, è un ottimo modo per cercare di scoprire ciò che potrebbe rifiutare o
non gradire il cliente al momento della presentazione del prodotto finale.
Spesso, infatti, si effettuano supposizioni su ciò che non sappiamo o che non
conosciamo adeguatamente. Tramite la conversazione tra tutte le parti in
causa è possibile, dunque, scoprire le possibili incomprensioni o
fraintendimenti che sono sorte tra coloro che sviluppano il software ed il
cliente.
LIVELLO 3: Inserire i risultati in uno o più test di accettazione
Scrivere i test di accettazione, una volta che sono stati ben definiti, non è
molto difficile; soprattutto se si utilizza un insieme di test. Analizzandoli, si
potrebbe riuscire a comprendere meglio ciò che ci è sfuggito
precedentemente.

LIVELLO 4: Organizzare i test per essere automatizzati


Alcuni insiemi di test effettuano, tutto questo, automaticamente. Sebbene
inizialmente occorrerà effettuare qualche lavoro in più del previsto per
automatizzare i test, successivamente otterremo enormi vantaggi. Non
rallenteremo più, infatti, la fase di sviluppo effettuando la regressione
manuale dei test.

Possiamo dire, dunque, che il 1° livello di ATDD richiede pochissimo sforzo e,


ciascuno dei livelli successivi, richiede un po’ più di lavoro rispetto al
precedente, ma restituisce ottimi risultati. Mentre inizialmente può sembrare
scoraggiante creare test di accettazione automatizzati, analizzando i diversi
livelli di ATDD possiamo renderci facilmente conto che la differenza di
investimento tra 2 livelli adiacenti è minima. Tuttavia, iniziare
immediatamente dal 4° livello di Acceptance Test Driven Development risulta
molto difficile e, per questo motivo, molti team di sviluppo iniziano dal 2°
livello.

Potrebbero piacerti anche