Rendere possibile un design incrementale è sicuramente uno dei maggiori benefici dei test di unità scritti alla TDD. Infatti un design incrementale passa da un continuo refactoring del codice ed è proprio qui che i test ci vengono in soccorso, fornendoci il coraggio ed il feedback necessario per migliorare costantemente il codice.
Un’applicazione con un alto indice di copertura arriva tipicamente ad avere almeno un ratio di 1:2 tra test statements e code statements. Questo significa aver scritto un client della nostra applicazione che come tutti i client risentirà in modo più o meno pesante delle modifiche del server. E’ chiaro quindi che se non si prestano le dovute attenzioni alla struttura delle API come dei test stessi, i test potrebbero diventare il boomerang del refactoring, trasformandosi in una zavorra al cambiamento invece che incoraggiarlo ed esserne a supporto.
Vale quindi la pena insistere pesantemente sui test? Sicuramente sì. Di seguito infatti una dimostrazione di come i test possono rivelarsi molto preziosi per testare non solo l’assenza di bug ma anche il design del codice di produzione!
Innanzitutto ecco per la mia esperienza quali attenzioni prestare per avere test ben scritti dal punto di vista della risposta al cambiamento del codice testato. Vedremo inoltre come molti dei problemi negli unit test possano rivelare problemi di design nel codice testato:
- Testare in isolabilità:
- se un test di unità testa indirettamente altri oggetti, sarà soggetto a modifica ogni volta che questi oggetti verranno rifattorizzati o modificati;
- inoltre sarà più difficile identificare eventuali errori nel codice dovuti al refactoring e ci perderemmo molti dei benefici del TDD
- Smell nel codice di produzione:
- a volte un test indiretto/coarse-grain è sintomo di scarsa information hiding nel codice oggetto di test
- Usare mock e/o stub soprattutto (ma non solo!) per lo strato di persistenza: l’accesso al db ha senso solo per i test che insistono sui DAO, non per inizializzare le classi di dominio o per fornire oggetti necessari ai servizi: abbiamo fatto tanto per creare un’applicazione a livelli, ed ora accoppiamo la persistenza con la BL!
- Smell nel codice di produzione:
- non utilizzare stub o mock potrebbe essere sintomo del timore degli sviluppatori che gli oggetti “reali” non interagiscano correttamente, ma per questo esistono i test di accettazione/di sistema!
- di contro un eccesso di mock o un eccesso di inizializzazione di mock può essere sintomo della violazione della Legge di Demeter (Tell, Don’t Ask!)
- Smell nel codice di produzione:
- Anche i test devono essere chiari e rispettare il Single Responsability Principle (SRP):
- la regola (non legge) “una assert per metodo” aiuta a rendere il codice di test pulito e focalizzato su piccoli aspetti del codice
- un metodo di test dovrebbe testare un solo comportamento stressando un solo metodo dell’oggetto sotto test
- i metodi di un TestCase condividono tra loro i metodi di setUp e di tearDown, in altre parole condividono la fixture! Fate quindi suonare un campanello di allarme quando avete i metodi di un TestCase che al loro interno creano ulteriori fixture: può essere un segnale che andrebbe creata un’altra classe TestCase. In modo simile attenzione ai setUp troppo generici che inizializzano oggetti non utilizzati da tutti i metodi del TestCase.
- Smell nel codice di produzione:
- se un test case non rispetta SRP, può essere lo specchio di una violazione dello stesso principio nel codice oggetto di test
- un setUp troppo grande può essere sintomo di un oggetto di produzione con un indice di efferent coupling eccessivo: ad esempio reificare la collaborazione tra gli oggetti
- Eliminare le duplicazioni tra i test:
- rifattorizzare le asserzioni
- attenzione però all’ereditarietà tra TestCase: anche con i test, l’ereditarietà è rischiosa e crea dipendenze tra i test spesso non desiderabili.
- Smell nel codice di produzione:
- anche in questo caso spesso patterns ripetuti tra vari TestCase rispecchiano una duplicazione nel codice oggetto del test! Ad esempio se ho fatto copia incolla di una classe A in una classe B, avrò tipicamente anche i test di B incollati dai test di A e modificati a-posteriori solo in piccola parte! One and Only Once!
- Testare l’interfaccia, non i metodi privati:
- in questo modo il test non dipende dai dettagli implementativi della classe
- Smell nel codice di produzione:
- se abbiamo troppo frequentemente la tentazione di testare i metodi privati, forse la complessità procedurale del nostro codice è eccessiva, o il metodo privato in realtà andrebbe reso pubblico magari in un’altra classe
- Tanti ma non troppi: eccellente garantire un’alta copertura del codice da parte dei test, ma attenzione a non scrivere test che non aggiungono nulla rispetto a quelli già scritti:
- terminata l’implementazione di una classe alla TDD, esaminare i test scritti eliminando quelli che, a design materializzato, appaiono inutili
- dare sempre un’occhiata ai test quando si rifattorizza il codice di produzione: a seguito di un refactoring, due test che testavano classi diverse ora potrebbero testare lo stessa cosa: eliminarne uno
I test altro non sono che client del codice che testano. E’ quindi fondamentale per evitare ripercussioni inaspettate sui test a fronte del cambiamento del codice di produzione, rispettare i principi di buon design anche nel codice sotto test. Di seguito le motivazioni di quest’affermazione:
- Open Closed Principle (il padre di tutti i principi):
- riflettere sempre sull’attuale chiusura al cambiamento del codice: le parti meno stabili dovrebbero essere isolate da quelle più stabili: in questo modo anche gli unit test stabili e quelli più instabili restano ben distinti.
- Single Responsibility Principle:
- se abbiamo una classe che “fa di tutto”, avremo di conseguenza molti unit test che dipendono da tale classe, e quindi a fronte del cambiamento su una responsabilità di tale classe, dovremo cambiare molti unit test
- Preferire la composizione all’ereditarietà:
- testare una classe C che eredita da B che eredita da A, significa che ad ogni cambiamento di B o di A, dovremo ritoccare anche i test di C! Alternativamente potremmo scrivere i test solo sulla classe radice A, a patto che venga rispettato il Liskov Substitution Principle.
- Avere interfacce piccole (Interface Segregation Principle):
- oltre a garantire Unit Test più chiari, permette ai client (e quindi ai test) di dipendere solo da un piccolo set di metodi
- Legge di Demeter:
- se un oggetto A dipende a cascata da molti altri oggetti, anche i test saranno soggetti a cambiamenti a cascata!
- Attenzione alle dipendenze cicliche (Acyclic Dependecies Principle):
- avere una dipendenza ciclica tra le classi non solo rende l’unità di riuso il ciclo e spesso opacizza il design, ma nel momento in cui si voglia aggiungere una classe all’interno del ciclo, può portare a strani errori, con test che si rompono a cascata e difficoltà di risolvere il problema se non con un pesante refactoring sul codice che coinvolgerà anche un gran numero di test.
Alcune risorse interessanti:



[OT] – Hai messo in cron il cambio di tema del blog una volta al giorno?
Comment di FRANK — 26 Settembre, 2006 @ 10:19 am
Tra qualche giorno un post che ne dà una spiegazione…
Comment di Enri — 26 Settembre, 2006 @ 1:40 pm
[...] Come già il buon FRANK ha notato, ultimamente il blog ha subito mutazioni di look giornaliere e da far girare la testa… [...]
Pingback di I fondamenti dell’Agilità e…i temi di wordpress « Enri Blog — 26 Settembre, 2006 @ 3:12 pm
Ho visto ora che esite il sito del testo che uscirà a breve “XUnit Test Patterns”, che affronta il tema del “come scrivere gli unit test”. Contiene un catalogo delle smell e dei refactoring da applicare.
Dategli un occhio!
XUnit Test Patterns
Comment di Enri — 6 Ottobre, 2006 @ 9:45 am
Aggiungo come risorsa anche il “Test Automation Manifest“. Da leggere anche questo!
Comment di Enri — 6 Ottobre, 2006 @ 9:48 am