Questo sito utilizza i cookie, anche di terze parti: cliccando su 'Chiudi', proseguendo nella navigazione, effettuando lo scroll della pagina o altro tipo di interazione col sito, acconsenti all'utilizzo dei cookie. Per maggiori informazioni o per negare il consenso a tutti o ad alcuni cookie, consulta l'informativa.

Stampa

Piccola storia.

La mattinata si presentava foriera di novità e cose nuove: la sera precedente avevo letto per la prima volta degli unit test, e incredibilmente avevo capito subito il meccanismo di funzionamento e le regole base per utilizzare lo strumento in modo efficace.

Ero intenzionato ad applicare quanto appreso nel team di cui facevo parte, così da rendere il software in sviluppo più testato e più stabile.

Con questi pensieri in testa avevo varcato il portone per andare in ufficio e, senza nemmeno prendere il caffè, mi ero precipitato alla scrivania per scrivere il mio primo unit test.

Avrei così fatto vedere al mondo intero e a miei colleghi come si usava lo strumento. Già sognavo l’approvazione del capo, nonché grandi apprezzamenti dei colleghi e magari, in mezzo a tutto quel clima di approvazione che avrei potuto creare, anche il numero di telefono di quello splendido pezzo di receptionista appena assunta (qualcosa tipo la foto a fianco) per poterla invitare fuori (erano anni in cui, per approcciare una ragazza, non si usavano i social....... ma si chiedeva il numero di cellulare).

Dopo aver iniziato a scrivere il primo test con la prima classe (tra l’altro la più semplice in fatto di algoritmo), mi rendo conto subito che le cose non sono così semplici come avevo immaginato.

Infatti la classe sotto attacco presentava svariate dipendenze con classi esterne, che rendevano semplicemente impossibile scrivere degli unit-test in modo lineare e semplice come invece mi ero immaginato.

Dopo una giornata di parolacce e preghiere (anche alternate consecutivamente) mi sono reso conto che gli unit-test vanno di pari passo con le classi fake, e/o un framework di mocking, come Moq (che, per inciso, all’epoca del racconto Moq ancora non esisteva, anzi forse il suo autore forse nemmeno era nato).

Questa necessità nella lettura serale non l’avevo compresa, sicuramente preso dall’entusiasmo.

Quanto sopra, con buona approssimazione, è la cronistoria di quando ho tentato la prima volta di usare gli unit-test, senza averne approfondito tutti gli aspetti.

Per terminare la storia la famosa settimana si è conclusa con me che riprendo ho ripreso in mano i “sacri testi” e studio con precisione anche le pagine dedicate alle fake-class.

Per la cronaca alla fine sono riuscito nell’intento, ma mi ci sono volute ancora diverse settimane di studio e refactoring del codice esistente e, manco a dirlo, la bella receptionista nel frattempo si era fidanzata con un mio collega, che non aveva perso tempo poiché la sera invece di studiare gli unit-test l’aveva invitata fuori.

La morale è semplice: se avete qualche bella ragazza (o ragazzo) da invitare fuori, state lontani dagli unit test.

 

Implementare gli unit-test in una classe reale

Nel seguito esporrò come implementare gli unit test in una classe “reale”: come vederemo che la cosa non è sempre possibile in modo diretto e facile.

In particolare se la classe stessa non segue i "buoni pattern di programmazione" occorrerà eseguirne il refactoring di alcune parti.

Incredibilmente uno dei vantaggi di utilizzare gli unit-test è che ci obbliga a implementare alcune buone pratiche di progettazione del codice.

Questo concetto può essere esemplificato con il seguente adagio

Se un metodo, per scrivere lo unit-test ti fa brontolare
significa che, per come è progettato, fa veramente vomitare

La classe reale che analizzerò si occupa di inviare delle email a una lista di indirizzi salvati su una tabella.

Per esporre il caso reale partiamo dalla funzionalità espressa nel seguito.

L’intento del codice di cui parleremo è quello di inviare una mail a una lista di iscritti (newsletter). Come e quando viene richiamato questo metodo non ci importa: l’importante è la funzionalità che questo implementa. Tutto si gioca sulla tabella nel seguito.

Nome CampoTipo
Mail varchar(100)
MailDaInviare bool

 

 

 

La procedura invierà solo le mail che hanno il flag MailDaInviare a true: una volta eseguita l’operazione di invio questo flag sarò posto a false.

Rimarco ulteriormente: come viene popolata e modificata questa tabella non ci interessa: diciamo solo che procedure esterne si occuperanno di essa (per esempio di reimpostare il falg MailDaInviare quando opportuno).

La classe sopra dovrà solo ed esclusivamente occuparsi di inviare le mail agli indirizzi che presentando tale flag a true.

Studiate un attimo il codice proposto: lo avete visto bene ?? Ci trovate nulla di strano ?? Tutto ok per Voi ??

Eppure il codice presentato non solo è assolutamente scandaloso, e ora Vi esporrò il perché, ma anche quasi impossibile da testare con gli unit-test.

Diciamo che il Vs software, cui la classe presentata sopra è parte costitutiva, è andato forte, e nel tempo avete sviluppato decine o centinaia di classi specializzate in vari contesti dell'applicativo e che sono scritte con lo stesso stile della schifezza presentata sopra.

Diciamo anche che un giorno Vi trovate nella necessità di cambiare il tipo che sottente l'applicativo, o anche per qualche ragione non volete più usare Entity framework come ORM .…..non è che abbiamo stretto un patto di sangue con M$, magari un domani potreste cambiare idea e adottare altri strumenti.

Ebbene convincere il Vs progetto ad adottare un nuovo sistema di accesso alla base dati diventa un'operazione estremamente difficoltosa: infatti occorrerebbe rivedere ogni classe che interagisce con il database e cambiare le modalità di interazione con questa.

Questo perché il peccato originale del codice presentato sopra è che la logica di accesso al database è inglobata dentro la tabella stessa.

Vabbè, e chi sene direte Voi .........difficilmente userò un altro ORM e/o database.

Ebbene osservo in tal caso che analoga osservazione può essere fatta anche per l’invio delle mail, e qui l’ipotesi per cambiare sistema non è affatto così remota.

In altri termini se un domani si vogliono inviare le mail usando un altro strumento (per esempio mailkit invece che system.net, che offre maggiori possibilità di personalizzazione) occorrerebbe andare in ogni classe per trovare eventuali occorrenze di utilizzo delle mail e apportare le modifiche necessarie.

Qui addirittura si è pensato di bene di inglobare anche la userid e password, ma gestire un modo differente per la gestione dell’account utilizzato cambia di nulla il problema, anzi lo peggiora.

Oss.: Nella pratica citati ora dovrebbe essere salvati in un qualche forma di repository criptato: questo ulteriore codice aggiungerebbe altra logica estranea allo scopo della classe e del suo metodo.

Ultimo ma non ultimo una nota di carattere filosofico. La classe e il metodo esposto si occupa di troppe cose: i sacri testi recitano che ogni modulo software debba occuparsi di solo una cosa, e possibilmente bene (vedi programmazione S.O.L.I.D.).

Qui invece abbiamo mischiato la logica di funzionamento per le newsletter con quella per l’invio delle mail. Quando si dovrà andare nella ricerca delle cause di un errore (magari sporadico) ci sono troppe logiche da analizzare !

Cosa dire poi del codice duplicato ? La logica di gestione alla base dati e per l’invio mail, così come l’eventuale gestione errori, è ridondato per ogni classe che esegue analoghe funzioni.

Insomma in definitiva un vero e proprio scandalo, la "waterloo” del programmatore.

Oltre a tutto questo il metodo è assolutamente non testabile con gli unit-test: infatti per le dipendenze con fattori esterni ogni esecuzione del test dovrebbe sempre avere prontamente disponibile una base dati connessa, ma anche la possibilità di inviare mail.

Detto in altri termini la logica implementata in InvioMailNewsLetter ha delle dipendenze molto forti (quelli bravi le chiamano anche strong dependency) con servizi esterni (datatabase, presenza server SMTP e connettività per invio mail).

Quindi prima di precedere oltre occorre eseguire il refactoring della schifezza sopra non solo per rendere il tutto più dignitoso, ma anche per permettere l’implementazione degli unit-test.

Primo refactoring

Sembra di aver complicato un po' tutto, ma in realtà abbiamo dato all’architettura un aspetto più estensibile. In pratica dentro la classe si programma l’invio delle mail usando l’interfaccia, e non un’istanza di una classe.

L’istanza del’oggetto viene passato dall’esterno nel costruttore: l’oggetto stesso in questo caso viene chiamato concrete object, e l’azione viene appellata con il nome di dependency injection.

Oss.: Segnalo che eseguire un dependency injection di un concrete object via costruttore non è l’unico modo possibile per passare dipendenze  dall'esterno: infatti esistono svariati sistemi, tutti validi, nonché delle librerie apposite che facilitano l'attività. Io preferisco comunque il constructor injection per mio gusto personale.

Pare evidente la possibilità di cambiare classe passata al costruttore, proponendo una nuova classe sempre in grado dii implementare l’interfaccia IinvioMail: poco importa come questa sia implementata (cioè con che strumenti sia implementato il metodo bool SendEmail(string destinatario, string _TestoEmail).

Questo ci supporta se per caso si dovesse cambiare qualcosa nel modo in cui si inviano le mail: la logica sarebbe concentrata in un solo punto, e solo qui sarebbe necessario eseguire delle modifiche. Inoltre il codice sarebbe più snello e perfomante: si evita anche duplicazione di codice e logiche di funzionamento.

Inoltre questa potenzialità sarà sfruttata negli unit-test, dove passaremo una classe finta modificata allo scopo.

Linkografia

Informatica pressapochista - Un viaggio negli unit-test: Introduzione - Prima parte

Informatica pressapochista - Un viaggio negli unit-test: xUnit e dintorni – Seconda parte

Dependency Injection - Tutorial Teacher

Understanding Dependency Injection Using Constructor, Property, And Method In C#