Un test scritto dopo il codice tende a verificare quello che il codice fa, non quello che doveva fare. Il test-driven development risolve il problema invertendo l’ordine: scrivi il test, guardalo fallire, poi scrivi il minimo codice che lo fa passare. Dato che il test esiste prima del codice che controlla, non può confermare silenziosamente qualunque cosa il codice restituisca. Kent Beck ha formalizzato questo ciclo come red, green, refactor nel suo libro del 2002 Test-Driven Development: By Example. Il nome è rimasto.
Cosa fa davvero il ciclo red-green-refactor
Ogni passaggio del ciclo ha tre fasi, e saltarne una lo trasforma in qualcos’altro.
Red. Scrivi un test per un comportamento che ancora non esiste, poi eseguilo. Deve fallire. Se passa subito, o il comportamento c’è giĂ , o il test non sta verificando niente di reale. Un’esecuzione rossa è la prova che il test può intercettare il bug che dovrebbe intercettare.
Green. Scrivi la minima quantità di codice di produzione che fa passare il test. Non il codice migliore. Il codice minimo. Restituire un valore hardcoded è un passo green legittimo, purché tu sappia che è quello che hai fatto e non ti fermi lì.
Refactor. Con il test verde, ripulisci quello che hai appena scritto: elimina la duplicazione, o estrai una funzione se è giustificato. Il test deve restare verde per tutto il tempo, perché è così che sai che il comportamento non è cambiato, solo la forma del codice.
Poi scegli il prossimo comportamento e ricomincia. Un’iterazione richiede pochi minuti: un ramo if, un caso limite, mai un’intera funzionalitĂ .
Un esempio di TDD in Python: la kata FizzBuzz
FizzBuzz è abbastanza piccola da farti vedere il ritmo invece del problema. Dato un numero, restituisce "Fizz" se è divisibile per 3, "Buzz" se è divisibile per 5, "FizzBuzz" se è divisibile per entrambi, altrimenti il numero stesso.
Inizia dal caso che non è nessuno di questi:
| |
| |
Rosso, e non hai dovuto costruire apposta il fallimento: fizzbuzz non esiste ancora, quindi pytest non riesce nemmeno a importare il modulo di test. Resta comunque un rosso valido: la correzione è la stessa in entrambi i casi. La cosa minima che lo fa passare:
| |
| |
Verde. Prossimo comportamento: i multipli di 3.
| |
Fallisce (assert '3' == 'Fizz'), e la correzione minima è un solo ramo:
| |
I multipli di 5 seguono la stessa forma, quindi aggiungi il test e l’elif corrispondente. Poi scrivi il caso che i primi due rami, da soli, non gestiscono ancora: un numero divisibile per entrambi.
| |
| |
15 % 3 == 0 viene controllato per primo, quindi la funzione restituisce "Fizz" prima ancora di guardare 15 % 5. Qui l’ordine conta. La correzione è un ramo per 15 che viene eseguito prima degli altri due:
| |
| |
Di nuovo verde, e ora il file di test ha qualcosa che vale la pena rifattorizzare: quattro funzioni quasi identiche. Unirle in un unico test parametrizzato conta come un refactor tanto quanto qualunque cosa faresti su fizzbuzz.py, purché il verde regga per tutto il tempo.
| |
Il bug sull’ordine è emerso solo perchĂ© il test per 15 esisteva prima della correzione. Scrivi prima il codice e fermati a “3 dĂ Fizz, 5 dĂ Buzz, sembra fatto”: fizzbuzz(15) restituisce "Fizz" e niente, nella suite, lo segnala.
Perché il codice scritto test-first finisce debolmente accoppiato
Il TDD cambia il design prima che qualunque test venga eseguito, perchĂ© devi scrivere il codice in modo che il test possa chiamarlo. Prendi una classe che costruisce da sĂ© il proprio repository verso il database, esattamente il pattern con cui apre Cos’è la Dependency Injection. Non puoi passarle un test double, perchĂ© niente nella sua firma ti permette di sostituirlo. Scrivere il test prima spinge quella dipendenza in un parametro o in un argomento del costruttore prima ancora che il codice di produzione esista per resisterle. Non aggiungi l’accoppiamento debole dopo, come bonus. Senza, il test che stai per scrivere non parte.
La stessa pressione spinge le funzioni a fare una cosa sola. Se un test deve impostare tre pezzi di stato non collegati tra loro per verificare un comportamento, la funzione sotto test fa tre cose non collegate tra loro. Dividila, e il test diventa piĂą corto.
Quando non usare il TDD
Il ciclo ha bisogno di alcune condizioni: conosci il comportamento che vuoi prima di scriverlo, i test vengono rieseguiti in pochi secondi, e il codice ha un punto in cui inserire un test double. Perderne anche solo una e il ciclo smette di ripagarsi. Anche quando valgono tutte, ha un punto cieco.
Non conosci ancora la forma della cosa. Se stai esplorando un’API che non conosci o stai facendo uno spike per vedere se un approccio è fattibile, scrivi prima il codice usa e getta. Impara qual è l’astrazione giusta, poi scrivi i test contro la versione che intendi tenere. I test per codice che stai per cancellare sono sforzo speso due volte.
Il ciclo di feedback è lento. Il TDD significa rieseguire i test dopo ogni piccola modifica. Una suite che colpisce un vero database o la rete a ogni esecuzione trasforma ognuna di quelle riesecuzioni in un’attesa. Test Unitari vs Test di Integrazione vs Test End-to-End spiega perchĂ© la velocitĂ a livello unitario viene dal sostituire quelle dipendenze. Se gli unici test che puoi scrivere per un pezzo di codice sono test di integrazione lenti, testarlo con il TDD significa aspettare, e le persone smettono di fare le cose che le fanno aspettare.
Non c’è ancora un punto d’innesto. Il codice legacy, con costruttori che raggiungono infrastrutture reali e funzioni che si chiamano direttamente a vicenda, non può essere sviluppato con il TDD da dove si trova. Non c’è dove mettere un double. Cos’è lo Spaghetti Code copre il modo per entrarci: un characterization test che fissa il comportamento esistente per primo, scritto contro il codice così com’è. Una volta che quella rete esiste, red-green-refactor funziona per la modifica successiva.
Il mocking eccessivo lascia passare un design scadente comunque. Un test che fa il mock di ogni collaboratore può diventare verde su una funzione che internamente è un disastro, purchĂ© le sue chiamate corrispondano a quello che i mock si aspettano. Il TDD tiene la tua attenzione sull’unica unitĂ che hai davanti. Ogni tanto devi comunque fare un passo indietro e chiederti se le unitĂ hanno la forma giusta.
Come iniziare con il TDD: correggi un bug esistente
Per il primo tentativo scegli un bug, non una funzionalitĂ . Scrivi un test che lo riproduce: chiama la funzione con l’input che lo rompe, verifica l’output che vuoi davvero ottenere, e guardalo fallire per il motivo giusto prima di toccare la correzione. La correzione diventa il passo verde, e il test resta nella suite così il bug non può tornare senza che nessuno se ne accorga. Fai un commit per ogni passaggio. Una scia di commit piccoli e sempre verdi è quello che rende git bisect veloce la prossima volta che qualcosa si rompe e nessuno ricorda dove.