Quello del software testing non è un argomento nuovo per uno sviluppatore, dopotutto chi non ha mai fatto dei test? Tutti sappiamo cosa sono e come scriverli.
In realtà, per quanto vecchiotto e “già sentito”, non è proprio un argomento così banale, anzi. Più passa il tempo, più mi rendo conto che non è affatto sufficiente aver scritto qualche test per padroneggiare l’argomento. Ci vuole molta pratica per scriverli in modo efficace e comunque non si smette mai di imparare.
Potessi tornare indietro nel tempo a quando ho iniziato a sviluppare, mi avrebbe fatto comodo conoscere alcuni concetti base. Non è mai troppo tardi comunque, spero che quanto ho imparato, argomento di questo articolo, possa regalarvi qualche spunto.
Software testing: alcuni chiarimenti
Scrivere bene i test non è scontato, non basta scriverne tantissimi o avere un’alta percentuale di coverage (1). Quando ciò accade, il più delle volte è perché ci sono molti test inutili o scritti male. Certo, pochi test o una coverage bassa (sotto il 50%) sono sintomo di bug, ma non vale il contrario perché è molto facile ottenere numeri alti con test di poca o addirittura nulla utilità. La coverage va usata per capire quali sono i punti nel nostro codice dove i test scarseggiano e quello a cui dovremmo puntare non è avere tanti test, ma averne di qualità.
Eppure i test sono il “coltellino svizzero” dello sviluppatore:
- Danno un feedback rapido e automatico per salvarvi dal rilasciare bug in produzione.
- Permettono di mettere mano a codice scritto da altri e che non conoscete, senza il rischio di fare troppi danni.
- Consentono di modificare il tanto temuto “codice legacy”, perché scrivere tanti test potrebbe essere la vostra unica ancora di salvezza. A tal proposito, conoscete la tecnica del Golden Master Testing (2)?
- Possono guidarci nello sviluppo di nuove funzionalità, come TDD (3) ci insegna
- Permettono di fare refactoring di codice, attività che, senza una copertura di test, non potreste fare limitandovi quindi a quelli automatici offerti dal vostro IDE.
- Aiutano a migliorare il design del codice: scrivendo uno unit test e facendo fatica a concepirlo, vi accorgete di come modificare il codice dell’applicazione riducendone/spostandone la complessità per aiutarvi a scrivere il test; questo forza a rivedere e migliorare il design.
- Possono diventare la migliore documentazione per il vostro codice: leggerli vi aiuta a capire meglio le funzionalità e i casi d’uso previsti.
La piramide dei test automatici
Esistono diversi tipi di test: unit, system, integration, e2e, mutation, performance, stress, fault-injection, exploratory e molti altri.
E’ importante avere di test automatici. Nel libro Succeeding with Agile: Software Development Using Scrum (4) di Mike Cohn c’è un’immagine a mio avviso utile per farsi un’idea.
Nella piramide sono definiti tre layer: Unit > Service > UI
Nonostante sia un’immagine del 2009, rappresenta tutt’oggi quella che dovrebbe essere la situazione ideale dei nostri test automatici. Mi piace perché saltano immediatamente all’occhio alcuni concetti:
- Abbiamo più livelli, non bastano gli unit test.
- Dato che vengono scritti fin da subito, si ha una concentrazione di test maggiore per gli unit test che sono le fondamenta.
- Dal basso verso l’alto, si passa dal testare parti di codice in isolamento al testare sempre più l’interazione.
- Più si sale verso la punta della piramide, più tempo viene richiesto per scrivere ed eseguire i test. Ecco perché tendiamo ad avere meno test end-to-end, o comunque è buona norma scriverne pochi.
Software testing: chi dovrebbe occuparsene e quando?
Per quanto riguarda la parte bassa della piramide, sta a noi sviluppatori scrivere unit test ogni volta che sviluppiamo nuovo codice (ricordatevi sempre TDD). Tenete presente che più ritardiamo nello scriverli e più diventa lungo e complicato. Il testing è una parte fondamentale del nostro lavoro quindi prendiamoci il tempo di farlo, già in fase di pianificazione delle nostre story è bene tenerne conto.
Per la parte più alta, quindi UI test o end-to-end test, dipende. Potreste essere così fortunati da avere uno o più tester che se ne occuperanno al posto nostro, altrimenti parlatene all’interno del team così da valutare assieme se e quando farli.
Software testing: dobbiamo sempre farlo?
C’è chi sostiene che non servano, chi ritiene che sia necessario testare solo le parti più critiche e chi sostiene che tutto il codice deve avere il suo buon numero di test e la coverage deve essere, quanto più possibile, vicina al 100%.
La mia idea è che “più siamo coperti da test e più viviamo felici”, ma devono essere sensati e di valore. Mi rendo conto di essere ripetitivo però mi è capitato di vedere più di un progetto dove, per quanto i numeri di test e coverage fossero alti, la loro utilità non lo era altrettanto.
Purtroppo avere test di qualità è costoso, richiede tempo sia svilupparli che mantenerli. Credo che un buon metodo per valutare quando è il caso di fare un test è quello di valutarne i costi/benefici. A tal proposito riporto di seguito un grafico del quale non ricordo la fonte:
L’ascissa rappresenta quanto ci costa realizzare il test; più è complesso e pieno di dipendenze e maggiore sarà il costo.
L’ordinata rappresenta il valore della realizzazione del test (complessità e importanza a livello di business logic)
Dal grafico emergono quattro categorie:
- Primo quadrante (-costo, +beneficio): caso semplice, è quando ci conviene di più fare i nostri test.
- Secondo quadrante (+costo, -beneficio): altro caso semplice, è quando scrivere un test non porterebbe alcun valore aggiunto e inoltre sarebbe complicato da testare. Meglio lasciar perdere.
- Terzo quadrante (+costo, +beneficio): è il caso più complicato. Vorremmo avere dei test perché è una parte di codice complessa e di fondamentale importanza ma testarlo è altrettanto complicato. Il fatto che sia complesso scrivere test è di per sé un caso di code smell (5) e forse dovremmo ri-progettare meglio quella funzionalità: ricorrere agli oggetti se stiamo sviluppando in Java (o comunque Object Oriented Programming) e isolare verso l’esterno le parti che più ci bloccano dal fare i test. Abbiamo detto che è una parte che non vogliamo lasciare senza test, vale la pena investirci del tempo.
- Quarto quadrante (-costo, -beneficio): questo è il caso peggiore. Scriviamo i test e lo facciamo in fretta, senza trarne vantaggio. Scriverli o meno è abbastanza indifferente, ma se davvero costa poco, perché non farlo?
Nei paragrafi successivi farò delle considerazioni sugli unit test e più in generale sul testing, fornendovi (spero) anche qualche buon consiglio.
Considerazioni sugli unit test
- Unit-test !== Integration test: un integration test serve per testare l’interazione tra moduli diversi, lo unit test ha invece lo scopo di testare il comportamento dei componenti minimi quindi il singolo metodo o la singola classe in isolamento. I test di integrazione sono di più alto livello e sono sicuramente più costosi da mantenere.
- Arrange Act Assert pattern (6): solitamente cerco di separare i miei test seguendo questo pattern. Non è obbligatorio ma credo che, usandolo sempre, aiuti a rendere più immediata la lettura dei test.
- Uno unit test deve essere piccolo e veloce da scrivere. Quando le righe di codice che lo compongono iniziano ad essere tante, i casi sono due: o non stiamo facendo uno unit test oppure, se proprio non riusciamo a fare un test più piccolo, potrebbero esserci problemi di design architetturale perciò dovremmo fare un po’ di refactoring per estrarre e separare la logica.
- Fare più di una
assert
in un test non è “il male”: in certi casi ha perfettamente senso ma in linea di massima cerco sempre di restare sulla singola assert per test. Meglio se in chiusura del test stesso. - Utilizzare il più possibile
assertEquals(...)
a discapito diassertTrue(...)
. Questo perché, in caso di test che fallisce, vogliamo avere quanti più feedback possibile sul motivo del fallimento. Supponiamo di avere qualcosa del genere come risultato:expected true actual false
. Non ci dice granché, inveceexpected “string” actual “ StrRing”
ci fa capire subito dove sta l’errore. - Il nome del test è importante perché quando li eseguiamo tutti assieme e ne troviamo uno rosso, ci piacerebbe capire subito dov’è il problema senza andare a leggere tutto il codice:
- trovo comoda la convenzione UnitOfWork_StateUnderTest_ExpectedBehavior (7). Prima che qualcuno salti dalla sedia, tenete presente che ce ne sono diverse con i loro pro e contro, provate a cercare un po’ su google. L’importante è che rappresenti bene cosa stiamo testando e con quali condizioni. Soprattutto però, “be consistent”, cioè adottiamo una convenzione e manteniamo la stessa per tutti i test.
- Non preoccupiamoci se il nome dovesse risultare molto lungo: meglio non lesinare sui caratteri a scapito della leggibilità.
- Prima di cominciare. Uno dei consigli per fare TDD è di partire analizzando la funzionalità per poi creare una lista dei test che vogliamo scrivere. É semplice ma è davvero un ottimo consiglio, quindi perché non usarlo sempre? Che il codice ci sia già o meno, prendere carta e penna o aggiungere un commento con una lista puntata dei test da implementare è un procedimento che aiuta a ragionare meglio e ci guida nello sviluppo.
- Mocking: a volte può aver senso usare dei mock, si trovano diverse librerie dedicate e la tematica potrebbe essere oggetto di altri articoli. Come per la lunghezza di un test, qualora dovessimo ritrovarci ad usare mock per tanti oggetti, allora avremmo sicuramente qualche problemino di design. E ricordatevi che vogliamo testare la nostra applicazione, non i mock, quindi non esageriamo.
Considerazioni generali
- Non solo per trovare bug: a volte risulta molto più veloce scrivere uno unit test che far girare tutto l’applicativo per fare attività di debug o aggiungere un log. E, se quel test scritto oggi, domani non dovesse servire più? Poco male, non ci trovo nulla di sbagliato a cancellare test che non sono più utili.
- Non affezioniamoci ai test. Se non serve o non è utile trattiamolo come se fosse codice commentato: eliminiamolo senza pietà.
- Test come documentazione: mi è capitato di dover lavorare a parti di codice critiche e anche piuttosto complesse. Per quanto scritte bene, non era facilissimo capire quali compiti assolvessero. Fortunatamente erano coperte da unit test scritti bene. Sono sono stati un’utilissima documentazione che mi ha fatto risparmiare tempo, altrimenti speso per rileggere il codice.
- I test e2e (end-to-end) sono utili perché verificano che tutti i moduli funzionino bene assieme e permettono di replicare la funzionalità simulando l’interazione con l’utente finale. Occhio però a non puntare solo su questi test. Riprendendo la piramide, attenzione a non capovolgerla facendo tanti e2e test e pochi unit test. Non è una buona idea perché:
- Mantenerli costa davvero molto, anche solo aggiungere un div o spostare un componente in un’altra posizione della UI può voler dire rompere i test automatici e doverci mettere mano per tornare a farli funzionare. Mantenere un test in isolamento su una piccola porzione di codice sarà sempre meno costoso rispetto al costo di un e2e test.
- Possono fallire per diversi motivi e non è sempre immediato risalire alla causa del fallimento perché testano molto codice quindi il bug potrebbe riguardare diversi moduli.
- A volte falliscono per motivi esterni alla nostra applicazione, potrebbe volerci un po’ per capirne la causa.
- Con dei test così grandi che testano un’intera funzionalità è facile che sfuggano i bug più piccoli.
- I tempi di esecuzione sono elevati e questo ci porta a non eseguirli spesso. Ciò porta ad un rallentamento del ciclo di feedback
Riferimenti
- Test coverage – Martin Fowler
- Articolo sul workshop su Golden Master Testing e altre tecniche
- Test Driven Development (TDD) – Agile Way
- Libro “Succeeding with Agile: Software Development Using Scrum”
- Code Smell – Martin Fowler
- Arrange Act and Assert (AAA) pattern – Medium
- Naming standards for unit tests