(articolo redatto da Andrea Pilato)
Il Test Driven Development (TDD) è una metodologia di test, dove l’idea è quella di scrivere i test prima di scrivere il codice di produzione. Fa parte dei metodi agili, che si prefissano l’obiettivo di risolvere i problemi delle metodologie precedenti, più rigide e costose.
CHE COS’È IL TESTING
Il collaudo di un prodotto software è un’attività imprescindibile, senza di essa infatti un qualsiasi prodotto risulterebbe nella maggior parte dei casi non utilizzabile.
Il testing, inteso tradizionalmente come modello waterfall, viene eseguito al termine di un progetto, quando spesso, per rientrare nelle scadenze previste l’attività di testing, viene eseguita superficialmente, con un alto rischio di mandare in produzione un prodotto che non soddisfi le richieste.
È proprio attraverso il testing che vengono a galla i difetti del prodotto, rivelandone i malfunzionamenti. Data l’importanza di questa fase, è comprensibile come essa occupi circa il 40% del costo totale di produzione di un prodotto.

EVOLUZIONE DEI PRINCIPI DEL TESTING
Ora che abbiamo una conoscenza base del testing e della sua importanza, vediamo come si è evoluto nel tempo:
- 1970: Si iniziano a definire i fondamenti teorici e approcci sistematici al testing. L’obiettivo è quello di dimostrare la correttezza dei programmi, con test perfetti quando non ci sono errori.
- 1980: Nascono i primi standard, con conseguente cambio di mentalità. L’obiettivo è quello di trovare errori, con la conseguenza che un test che non ne rivela è un test fallito.
- 1990: Cresce il connubio tra testing e qualità. Si affrontano nuove sfide come l’abbattimento dei costi della fase di test, come automatizzarne i processi ecc.
IL TESTING PRIMA DEL TDD
Come anticipato, prima della modalità agile, lo sviluppo del software avveniva in waterfall. Questo approccio prevede che le varie fasi per la produzioni di un prodotto siano a se stanti, passando alla successiva al termine della corrente.

La fase di sviluppo è completamente separata dal testing, questo permette agli sviluppatori di produrre più velocemente e di non avere meno carico lavorativo, dato che saranno demandati al team di tester.
A primo impatto potrebbe sembrare un approccio vincente, con gli sviluppatori che producono a pieno ritmo il codice che andrà in produzione, basato su delle specifiche date dalle fasi precedenti. Se teoricamente ogni fase dovrebbe essere priva di errori, nella realtà questo non succede, arrivati al testing gli errori vengono trovati tutti insieme, interrompendo il tutto il processo di sviluppo per ripartire dalla definizione dei requisiti.
COME IL TDD RISOLVE I VECCHI PROBLEMI
Il TDD è una delle 12 pratiche dell’extreme programming, che si basa su due principi:
- Test first programming, ovvero la stesura dei test avviene prima di quella del software;
- Refactor continui.
Intendere il TDD come esecuzione dei test prima dello sviluppo della funzionalità però è un modo semplicistico di spiegarlo, poiché definisce un vero e proprio cambio di mentalità, in cui occorre partire dal risultato attesto dall’utente.
Il TDD si basa su tre semplici regole:
- Non è permesso sviluppare codice di produzione finché non è stato scritto un test che fallisca;
- È sufficiente scrivere test che porti ad un fallimento o un errore di compilazione;
- Non è permesso scrivere più codice di quello sufficiente a far passare il test che fallisce.
Per poter meglio rispettare i principi del TDD possiamo seguire tre punti cardine:
- Test Early;
- Test Ofter;
- Test Automatically.
Test Early
Nel momento in cui deve essere aggiunta una nuova funzionalità al programma, lo sviluppatore prima di tutto definisce un primo caso di test, dopodiché passa alla definizione dell’interfaccia (sotto test), e solo dopo va ad implementare la funzione, limitando la scrittura del codice fino alla corretta esecuzione del test. Verificato che il nuovo test dia esito positivo, vengono rieseguiti i test definiti, che in caso di esito negativo prevedono la ricerca e la risoluzione dei bug nell’implementazione.
Quando tutti i test definiti risultato correttamente superati, si può procedere all’attività di refactoring, che ha come obiettivo quello di avere un codice performante e snello. Terminato il refactoring si eseguono nuovamente i test e in caso positivo può ritenersi completato il lavoro sulla nuova funzionalità.
Uno dei vantaggi è la sicurezza che lo sviluppatore abbia ben compreso i requisiti della funzione prima dell’implementazione, dovendo scrivere prima i casi di test. Non collocando più l’attività di testing alla fine, la scrittura dei test avviene nel modo più completo possibile, viene rilevata una buona quantità di bug e la qualità del software che viene consegnato è molto elevata. Infine, dovendo testare a priori un singolo metodo per volta, lo sviluppatore è portato a progettare il codice in parti poco interdipendenti tra loro, garantendo al software una certa modularità, oltre che una facilità nel riuso e nella manutenzione.
Dai processi precedenti, traspare il ciclo di vita del TDD:

La fase di sviluppo è completamente separata dal testing, questo permette agli sviluppatori di produrre più velocemente e di non avere meno carico lavorativo, dato che saranno demandati al team di tester.
A primo impatto potrebbe sembrare un approccio vincente, con gli sviluppatori che producono a pieno ritmo il codice che andrà in produzione, basato su delle specifiche date dalle fasi precedenti. Se teoricamente ogni fase dovrebbe essere priva di errori, nella realtà questo non succede, arrivati al testing gli errori vengono trovati tutti insieme, interrompendo il tutto il processo di sviluppo per ripartire dalla definizione dei requisiti.
Test Often
Come si evince, i test non vengono eseguiti una volta sola. Ad ogni aggiunta o modifica di una funzione vengono eseguiti non solo i test sulla parte modificata, ma anche quelli sul resto del software. La modifica alla funzione, o l’aggiunta di un metodo, verrà integrata nella versione corrente del software solo quando tutti i test risultano superati con successo.
Uno dei benefici di questa direttiva è la facilità di individuazione di bug poiché, eseguendo il test ad ogni piccola modifica del codice, è più facile circoscriverne la causa. Ripetere più volte il test consente di evitare la manifestazione nel software di bug precedentemente risolti che si ripresentano (test di regressione).
Test Automatically
Porre l’attività di testing al primo posto nel ciclo di sviluppo del software permette di ottenere una versione finale del prodotto richiesto di buona qualità, d’altro canto dover eseguire i test molto frequentemente potrebbe portare ad un rallentamento dei tempi di sviluppo. Questa problematica è solo apparente, infatti i test devono essere implementati in modo da poter essere eseguiti in automatico (cioè senza l’intervento dell’utente), con la possibilità di lanciare tutti i casi di test definiti con un singolo comando.
UN CASO D’USO REALE DEL TDD
Passiamo ad un esempio concreto, per vedere come mettere in pratica il TDD. Supponiamo il caso di dover calcolare i fattori primi di un numero, ovvero tutti i numeri primi che lo dividono esattamente con resto zero.
Es. fattore primo di 1 = 0; fattore primo di 2 = 2; fattori primi di 6 = 2, 3; fattori primi di 12 = 2,2,3.
Passiamo ora all’implementazione della nostra classe di test, che chiameremo PrimeFactorsTest, il cui primo test sarà un test vuoto.

Il primo test reale da aggiungere è quello per il numero 1, quindi definiamo un metodo “factorsOf” che prende in ingresso un numero intero e restituisce una lista di interi (i fattori primi del numero in input). Come da definizione il fattore primo di 1 è l’insieme vuoto, per noi una lista vuota.

Testiamo per 2, aspettandoci come risultato 2, ma con il codice attuale avremo una lista vuota, modifichiamo quindi il codice aggiungendo la nuova casistica.

Aggiungendo il caso di test per il numero 3, abbiamo 3 come risultato atteso, ma invece il nostro codice restituisce 2, quindi andiamo a modificare il codice.

Al test per 4 ci aspettiamo 2,2 come risultato ma abbiamo 4, apportiamo quindi le modifiche necessarie.

A questo punto proseguiamo con i test per 5, 6 e 7 i quali sono tutti correttamente funzionanti con il codice attuale.

Dal test per 8 ci aspettiamo 2,2,2 ma il valore restituito è 2,4 apportiamo ulteriori modifiche.

Con n=9 il risultato atteso è 3, 3 ma avremo 9. La prima modifica per far passare il test è quella di replicare il codice.

Il test è passato, ma il codice è ripetitivo e non risolve un problema generico ma solo dei casi specifici, effettuiamo quindi delle operazioni di refactor.

A questo punto il codice è sì generico, ma notando meglio le condizioni è possibile notare come ci siano due if(n>1), poiché il secondo può essere portato fuori dal while al livello dell’altro if, può essere eliminato, e i test continueranno a passare.

A questo punto vediamo come il codice è pulito e risolve problemi generici. Proviamo con un numero molto grande (2x2x3x3x5x7x11x11x13) e vediamo che il risultato è quanto atteso.

Conclusioni
Testa anche tu il codice con i seguenti requisiti:
- dato un input n compreso tra 1 e 100, il codice dovrà stampare tutti i numeri naturali compresi tra 1 ed n;
- al posto dei numeri divisibili per 3 dovrà stampare la stringa “Nex”;
- al posto dei numeri divisibili per 5 dovrà stampare la stringa “Soft”;
- al posto dei numeri divisibili sia per 3 che per 5 dovrà stampare la stringa “NexSoft”;
Per n = 16 il risultato atteso è il seguente: 1, 2, Nex, 4, Soft, 6, 7, ….., 14, NexSoft, 16.
Se anche tu vuoi occuparti di progetti di Software Testing
dai un’occhiata alle nostre opportunità di lavoro e conosciamoci subito!


