Debugging e testing
In questa pagina 5
Tipi di errore
| Tipo | Quando emerge | Esempio |
|---|---|---|
| sintattico | prima dell'esecuzione (Python) / in compilazione (C) | : mancante, ; mancante |
| di tipo | a run-time in Python, in compilazione in C (in parte) | "a" + 1 |
| a run-time | durante l'esecuzione | divisione per zero, indice fuori intervallo, NULL dereferenziato |
| logico | mai segnalato: risultato sbagliato | < invece di <=, accumulatore inizializzato male |
In C molti errori a run-time non vengono segnalati affatto (comportamento indefinito): il programma può funzionare per caso e fallire altrove.
Metodo
- Riprodurre il problema con l'input più piccolo possibile.
- Leggere il messaggio: in Python il traceback dal fondo (tipo, riga, catena di chiamate, vedi Eccezioni in PythonEccezioni e traceback, eccezioni predefinite più comuni, try/except/else/finally, raise per segnalare errori, propagazione lungo le chiamate.Eccezioni in Python →); in C il primo errore del compilatore (i successivi sono spesso conseguenze).
- Formulare un'ipotesi e verificarla osservando lo stato (valori delle variabili) nel punto sospetto.
- Restringere: dimezzare il codice o l'input finché non si isola la causa.
- Correggere la causa, non il sintomo, e aggiungere un test che avrebbe trovato il bug.
Strumenti
Stampe di controllo
print(f"{i = }, {somma = }") # stampa nome e valorefprintf(stderr, "i=%d somma=%d\n", i, somma); /* stderr non è bufferizzato */Da togliere a lavoro finito.
Debugger
Permette di fermare il programma in un punto (breakpoint), eseguire una riga alla volta e ispezionare le variabili.
| Azione | VS Code | pdb (Python) |
gdb (C, compilare con -g) |
|---|---|---|---|
| avvio | F5 | python -m pdb prog.py o breakpoint() nel codice |
gdb ./prog, poi run |
| breakpoint | clic a sinistra del numero di riga | b 12 |
break 12 |
| riga successiva | F10 | n |
next |
| entra nella funzione | F11 | s |
step |
| continua | F5 | c |
continue |
| valore | pannello Variabili | p x |
print x |
| stack delle chiamate | pannello Call Stack | w |
backtrace |
gdb su un programma che va in segmentation fault mostra, con backtrace, la riga esatta del crash.
assert
assert condizione, "messaggio" interrompe il programma se la condizione è falsa. Serve a verificare ipotesi interne (precondizioni, invarianti di ciclo), non a validare l'input dell'utente.
def media(v):
assert len(v) > 0, "lista vuota"
return sum(v) / len(v)#include <assert.h>
assert(n > 0); /* disattivato compilando con -DNDEBUG */Errori di memoria in C
gcc -Wall -Wextra -g -fsanitize=address,undefined prog.c: a run-time segnala accessi fuori dai limiti, uso dopofree, overflow di interi con segno.valgrind --leak-check=full ./prog: memoria non liberata e letture di memoria non inizializzata.
Testing
Un test esegue il codice su un input con risultato noto e confronta. I test mostrano la presenza di errori, non ne dimostrano l'assenza: vanno scelti bene.
Scegliere i casi
- Casi normali: qualche input tipico.
- Casi limite: sequenza vuota, un solo elemento, primo/ultimo elemento, valori uguali, zero, negativi, massimo intero in C.
- Partizioni: dividere gli input in classi che il codice tratta allo stesso modo (es. per la ricerca: elemento presente all'inizio, in mezzo, alla fine, assente) e testare almeno un caso per classe e i confini tra le classi.
- Input non validi: verificare che venga sollevata l'eccezione prevista o restituito il codice d'errore.
Test in Python
Con semplici assert in una funzione di test:
def test_ricerca_binaria():
v = [1, 3, 5, 7]
assert ricerca_binaria(v, 1) == 0 # primo
assert ricerca_binaria(v, 7) == 3 # ultimo
assert ricerca_binaria(v, 4) == -1 # assente, in mezzo
assert ricerca_binaria([], 1) == -1 # vuota
assert ricerca_binaria([5], 5) == 0 # un elemento
if __name__ == "__main__":
test_ricerca_binaria()
print("tutti i test superati")Con il modulo unittest della libreria standard:
import unittest
from algoritmi import ricerca_binaria, media # modulo con il codice da testare
class TestRicerca(unittest.TestCase):
def test_presente(self):
self.assertEqual(ricerca_binaria([1, 3, 5], 3), 1)
def test_assente(self):
self.assertEqual(ricerca_binaria([1, 3, 5], 2), -1)
def test_media_vuota(self):
with self.assertRaises(ValueError):
media([])
if __name__ == "__main__":
unittest.main()Con doctest gli esempi nella docstring diventano test (python -m doctest modulo.py -v):
def quadrato(x):
"""
>>> quadrato(3)
9
>>> quadrato(-2)
4
"""
return x * xTest in C
Senza librerie esterne, una funzione di test con assert chiamata da main:
#include <assert.h>
void test_lunghezza(void) {
assert(lunghezza("") == 0);
assert(lunghezza("a") == 1);
assert(lunghezza("ciao") == 4);
}
int main(void) {
test_lunghezza();
printf("ok\n");
return 0;
}Quando testare
Scrivere i test insieme al codice (o prima), rieseguirli dopo ogni modifica (test di regressione). Funzioni piccole che restituiscono valori invece di stamparli (Funzioni in Pythondef e return, parametri posizionali, con nome e con valore predefinito, *args e **kwargs; passaggio per riferimento a oggetto; ambito delle variabili LEGB; docstring, type hint, lambda.Funzioni in Python →) sono molto più facili da testare.
Errori tipici
- Correggere a tentativi senza un'ipotesi.
- Testare solo il caso "medio" e mai sequenze vuote o estremi.
- Usare
assertper controllare l'input dell'utente (in C sparisce con-DNDEBUG, in Python conpython -O). - Lasciare stampe di debug nel codice consegnato.