Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
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.
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
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.
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.
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.
“Il codice della classe di test è tanto importante quanto il codice da testare”
Implementazione fasulla
Triangolazione
Implementazione diretta
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à.
JUnit
JUnit è uno dei più importanti Java frameworks.
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.
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:
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.
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.
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.
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.
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.
Dummy Objects
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.
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.
Svolgimento
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
la somma delle monete di euro nel salvadanaio sia stata effettuata con
successo.
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.
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.
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:
SVANTAGGI:
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à.