I vantaggi principali dell’adozione degli unit test sono molteplici: occorre solo valutare se lo sforzo profuso viene ripagato dai vantaggi ottenuti.
> Gli unit test minimizzano l’eventualità che le modifiche introdotte possano in qualche modo inficiare il comportamento del software già rilasciato: detto in altri termini cercano di evitare che quando si eseguono delle modifiche per implementare nuove feature, o anche per risolvere qualche malfunzione, si introducano dei breaking change o anche veri e propri bachi su parti precedentemente funzionanti.
> Scrivere codice adatto per essere testato con gli unit test inevitabilmente obbliga ad utilizzare un’architettura il più possibile aderente ai migliori pattern di programmazione (vedi S.O.L.I.D. e KISS, e in particolare Dependency Injection e Inversion of Control).
> Per software che coinvolgono UI gli unit test favoriscono l’adozione di pattern di programmazione come MVVM o MVC, che disaccoppiano in modo consistente la parte UI dalla parte di logica, con tutti i benefici che questo apporta.
> Il codice per gli unit test documenta in modo chiaro e inequivocabile il funzionamento degli elementi del software, magari anche declinato a casi particolari o situazioni critiche.
....siete convinti ora ???
Oltre gli unit test
Giusto per essere chiari: occorre evidenziare che per un software articolato e di una certa complessità il solo utilizzo degli unit test non è da considerarsi sufficiente per considerare le attività di test complete.
Infatti sia le best-practice, che i sacri testi dell’ingegneria del software, affermano che occorre sempre affiancare a questi anche altri tipi di test.
Infatti uno dei limiti degli unit test è che si prendono pezzi critici del software e si esegue su ognuno di questi un test di funzionamento: quindi si esegue il test il funzionamento di ogni singolo elemento/atomo dell'applicativo.
Però un software è dotato di più elementi (classi, funzioni, moduli) che interagiscono tra loro, e non è affatto detto che mettendo insieme tutti i pezzi (pur singolarmente funzionanti) tutto funzioni nel modo voluto.
Quindi è necessario eseguire una serie di test volti a capire cosa succede una volta che si sono messi tutti i pezzi insieme.
Per eseguire questo tipo di verifiche è possibile agire in due modi.
> si mettono insieme TUTTI i componenti del software, ivi compresi eventuali elementi esterni, e si eseguono dei test per verificare il corretto funzionamento: in tal caso si parla di un End-to-End Test
> si mettono insieme solo un sottoinsieme di componenti per verificare se questo sottogruppo propone un corretto funzionamento: in tal caso si ha un Integration Test
Detto in termini pratici gli End-To-End test si eseguono in una installazione reale in un ambiente uguale a quello di produzione dove girerà il software in produzione. Diversamente è possibile pensare agli Integration Test come test eseguiti su un insieme di classi.
Un esempio poco evidente di Integration Test è quando si esegue un test anche automatico su una singola classe con dipendenze esterne, ma invece di sottoporgli per queste delle istanze di classi mock o fake si utilizzano invece le classi reali che saranno utilizzate in produzione.
Come questi ulteriori tipologie di test possano essere implementate è al di fuori dello scopo della mia serie: basti dire che esistono diversi strumenti e framework utili allo scopo.
Aggiungo che nonostante tutti questi test possano essere automatizzati, secondo me una parte di essi, seppur piccola, debba essere eseguita in modo manuale, da operatori umani esperti del settore di utilizzo dell'applicativo e del suo contesto di utilizzo.
Programmare “verso” gli unit test
L’utilizzo degli unit test propone un modello di sviluppo diverso e ritengo, in molti casi più efficace.
Questo modello, in estrema sintesi, si basa sullo scrivere prima di ogni cosa le logiche di business, cioè le classi che propongono le logiche di funzionamento. Queste possono essere facilmente testate e azionate tramite gli unit test: solo terminata questa parte, è possibile affrontare l’integrazione delle logiche create com al altre parti costitutive del software (UI, controller o altri consumer).
Questo approccio viene chiamato Test First, o anche Test-Driven Development (TDD), in opposizione al solito metodo di sviluppo che viene invece chiamato Code First.
Quindi se si vuole seguire il TDD la metodologi da seguire è la seguente.
> Identificare una serie di test-case, cioè di casi di funzionamento normale degli algoritmi da implementare con le relative classi da scrivere, nonché di una serie di casi particolari.
> Scrivere per ognuno di questi i relativi unit-test, partendo dal più semplice al più difficile.
> Dopo aver scritto ogni unit-test scrivere il relativo codice di produzione delegato a implementare l’algoritmo nonché a soddisfare lo unit-test relativo.
Quanto sopra è un approccio “diverso” allo sviluppo del software, che permette di ottenere svariati vantaggi, molto dei quali sono stati esposti prima.
Aggiungo che seguire un sistema di questo genere permette di dotarsi già dalle prime fasi di sviluppo del software di una serie di unit test che saranno utili anche nelle fasi future dello sviluppo del codice, quando si dovrà modificare il codice esistente per nuove implementazioni.
La buona pratica suggerisce che questi unit test dovrebbero poi essere integrati via via quando si risolvono bug o si trovano malfunzioni al codice stesso: questi nuovi test dovrebbero essere scritti proprio per evidenziare il difetto trovato e successivamente risolto.
Quindi quello che è possibile affermare è che la "filosofia" che sottende gli unit test può anche diventare una pratica di sviluppo, che è centrata intorno al test continuo dei componenti funzionali principali del software isolandoli dal contesto di funzionamento.
E per software già esistenti ??
Quanto scritto sopra è bello ma…. Vale solo per software che si scrivono da zero. E per l’esistente ?
Se per caso avete preso in eredità qualche progetto senza alcun unit test e reputaste utile aggiungerli ?
Beh, qui le cose si fanno complicate. Scrivere unit test per codice esistente è assolutamente possibile, ma come visto spesso per implementarli occorre fare un refactoring dell’esistente.
Per esempio se non si è eseguito con religiosa cura il repository pattern è difficile scrivere degli unit test per metodi che hanno integrato al loro interno l’accesso a database.
Quindi cosa fare ? Non esiste una risposta univoca e ogni idea è valida. Il concetto principale è che eseguire il refactoring di codice legacy funzionante è sempre un processo molto dispendioso nonché foriero di bug e malfunzioni impreviste, e pertanto da evitare sempre e comunque (….siamo a Genova e l’argomento costi è sempre visto con particolare attenzione).
Mi permetto di ricordare ai più giovani di me il “caso Netscape”: Vi invito a leggerne qualcosa in giro, è molto istruttivo (vedere linkografia).
Pertanto il mio approccio sull’argomento segue il seguente sacro principio: codice funzionante non si tocca per nessun motivo.
Quindi come aggiungere a codice esistente e funzionante degli unit-test ? Semplice: solo quando occorre mettere mano a una parte degli algoritmi al fine di risolvere bug o fare nuove importanti implementazioni, e solo e soltanto in questo caso.
Ma ovviametne è una mia opinione personale.
Linkografia
Informatica pressapochista - Un viaggio negli unit-test: Introduzione - Prima parte
Informatica pressapochista - Un viaggio negli unit-test: xUnit e dintorni – Seconda parte
Informatica pressapochista - Un viaggio negli unit-test: Fake class e dintorni - Terza parte
Informatica pressapochista - Un viaggio negli unit-test: Repository pattern - Quarta parte
Informatica pressapochista - Un viaggio negli unit-test: Non male le fake class.... – Quinta parte
Informatica pressapochista - Un viaggio negli unit-test: Un viaggio negli unit-test: Ci serve Moq ? – Sesta parte
Informatica pressapochista - Un viaggio negli unit-test: E in Xamarin ? Con FreshMVVM ! – Settima parte
Informatica pressapochista - Un viaggio negli unit-test: Il navigator in Xamarin secondo FreshMVVM – Ottava parte
Medium - Lessons from 6 software rewrite stories
Joel on Software - Things You Should Never Do, Part I
GitHub - FreshMvvm