Davanti a un bug, l'istinto porta quasi sempre nella stessa direzione: modificare qualcosa a caso, rieseguire, sperare. A volte funziona, e proprio per questo l'abitudine resiste. Ma quando il problema è serio, procedere per tentativi può far perdere giorni. Il debugging efficace non è una questione di intuito geniale: è un metodo, molto simile a quello scientifico, che chiunque può imparare e applicare.
Primo passo: riprodurre il problema
Un bug che non sai riprodurre è un bug che non puoi correggere con certezza. Prima di toccare il codice, cerca di rispondere a domande precise:
- quali sono esattamente i passi che portano all'errore?
- succede sempre o solo a volte?
- succede con tutti i dati o solo con alcuni?
- succede in tutti gli ambienti o solo in uno (produzione, un certo browser, un certo sistema operativo)?
L'obiettivo è ottenere una riproduzione affidabile, idealmente un comando o un test automatico che fallisce ogni volta. Quando ci sei riuscito, hai già fatto metà del lavoro.
Leggere davvero il messaggio di errore
Sembra banale, ma molti bug vengono cercati per ore mentre la risposta è scritta nel messaggio di errore. Leggilo per intero, con calma: tipo di eccezione, messaggio, file e numero di riga. Se c'è uno stack trace, individua la prima riga che appartiene al tuo codice e non a una libreria: spesso è lì che il problema ha origine.
Formulare ipotesi e verificarle
Il cuore del metodo è un ciclo semplice:
- osserva il comportamento errato;
- formula un'ipotesi precisa sulla causa («il totale è sbagliato perché lo sconto viene applicato due volte»);
- progetta una verifica che confermi o smentisca l'ipotesi;
- esegui la verifica e accetta il risultato, anche se smentisce la tua idea preferita.
La differenza con il procedere per tentativi è sottile ma decisiva: ogni prova ti insegna qualcosa. Anche un'ipotesi smentita restringe il campo. Annotare su un foglio le ipotesi già escluse evita di ripetere le stesse verifiche e aiuta a ragionare quando il problema si complica.
Ridurre il caso
Un bug che si manifesta in un sistema complesso è difficile da analizzare. Conviene ridurlo progressivamente al caso minimo che lo riproduce:
- se l'errore avviene con un file di diecimila righe, prova con metà, poi con un quarto;
- se coinvolge una pagina con venti componenti, rimuovili uno alla volta;
- se dipende da una sequenza di operazioni, elimina quelle non necessarie.
Spesso, mentre riduci il caso, la causa diventa evidente da sola. E se non succede, il caso minimo è perfetto da condividere con un collega o da allegare a una segnalazione.
Gli strumenti
| Strumento | Quando usarlo |
|---|---|
| Stampe e log | Verifiche rapide del valore di una variabile in un punto preciso |
| Debugger | Seguire l'esecuzione passo passo e ispezionare lo stato completo |
| Breakpoint condizionali | Fermarsi solo quando una condizione è vera, per esempio all'iterazione 500 |
| Test automatici | Fissare la riproduzione e verificare la correzione |
git bisect |
Trovare il commit che ha introdotto una regressione |
Il debugger merita un investimento di tempo. In Python basta inserire breakpoint() in un punto del codice per fermare l'esecuzione e ispezionare variabili, eseguire espressioni e avanzare riga per riga. Gli editor moderni offrono la stessa funzione con un'interfaccia grafica.
La bisezione
Quando sai che una versione precedente funzionava e quella attuale no, git bisect automatizza una ricerca binaria nella storia:
git bisect start
git bisect bad # la versione attuale ha il bug
git bisect good v2.1.0 # questa versione funzionava
# Git propone un commit intermedio: lo provi e rispondi
git bisect good # oppure: git bisect bad
# ... ripeti finché Git indica il commit colpevole
git bisect reset
Con mille commit in mezzo, bastano circa dieci verifiche per trovare quello responsabile. Se hai un test che riproduce il bug, puoi persino affidare tutto a git bisect run.
Spiegare il problema ad alta voce
Una tecnica sorprendentemente efficace è descrivere il problema, riga per riga, a qualcuno, anche a un oggetto sulla scrivania: è il cosiddetto rubber duck debugging, dal nome della paperella di gomma usata come ascoltatore. Costringersi a spiegare cosa dovrebbe fare ogni riga rivela spesso il punto in cui la realtà diverge dalle aspettative.
Dopo la correzione
Trovata la causa, il lavoro non è finito:
- scrivi un test che fallisce senza la correzione e passa con essa;
- chiediti se lo stesso errore può esistere in altri punti del codice;
- verifica di aver corretto la causa e non solo il sintomo;
- nel messaggio di commit, spiega cosa provocava il bug.
In sintesi
- Prima di correggere, ottieni una riproduzione affidabile del problema.
- Leggi per intero il messaggio di errore e lo stack trace.
- Procedi per ipotesi verificabili, non per tentativi casuali, e annota ciò che hai escluso.
- Riduci il caso al minimo e usa debugger e
git bisectper restringere la ricerca. - Chiudi ogni correzione con un test che impedisca al bug di tornare.