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:

1
2
3
4
5
6
# test_fizzbuzz.py
from fizzbuzz import fizzbuzz


def test_returns_the_number_as_a_string():
    assert fizzbuzz(1) == "1"
1
2
3
4
$ pytest -q
ImportError while importing test module 'test_fizzbuzz.py'.
E   ModuleNotFoundError: No module named 'fizzbuzz'
1 error in 0.01s

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:

1
2
3
# fizzbuzz.py
def fizzbuzz(n):
    return str(n)
1
2
3
$ pytest -q
.
1 passed in 0.01s

Verde. Prossimo comportamento: i multipli di 3.

1
2
def test_returns_fizz_for_multiples_of_three():
    assert fizzbuzz(3) == "Fizz"

Fallisce (assert '3' == 'Fizz'), e la correzione minima è un solo ramo:

1
2
3
4
def fizzbuzz(n):
    if n % 3 == 0:
        return "Fizz"
    return str(n)

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.

1
2
def test_returns_fizzbuzz_for_multiples_of_fifteen():
    assert fizzbuzz(15) == "FizzBuzz"
1
2
3
4
$ pytest -q
...F
E   assert 'Fizz' == 'FizzBuzz'
3 passed, 1 failed in 0.01s

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:

1
2
3
4
5
6
7
8
def fizzbuzz(n):
    if n % 15 == 0:
        return "FizzBuzz"
    if n % 3 == 0:
        return "Fizz"
    if n % 5 == 0:
        return "Buzz"
    return str(n)
1
2
3
$ pytest -q
....
4 passed in 0.01s

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import pytest

from fizzbuzz import fizzbuzz


@pytest.mark.parametrize(
    "n, expected",
    [
        (1, "1"),
        (3, "Fizz"),
        (5, "Buzz"),
        (15, "FizzBuzz"),
    ],
)
def test_fizzbuzz(n, expected):
    assert fizzbuzz(n) == expected

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.

Articoli correlati