Corso Python / Test automatici con unittest e controlli di integrazione
I test automatici verificano che determinati ingressi producano il risultato o l’eccezione previsti. Con unittest puoi controllare una funzione Python, coprire i casi limite e accorgerti di una regressione dopo una modifica.
Definire il comportamento da proteggere
La funzione total di questa lezione riceve una lista di interi non negativi. Restituisce la somma, restituisce zero per una lista vuota e rifiuta numeri negativi, valori booleani e stringhe con ValueError. Il controllo type(x) is not int esclude esplicitamente bool, che in Python è una sottoclasse di int.
Partiamo da un calcolo senza rete, orologio o file dell’utente: gli stessi dati devono produrre lo stesso risultato. Un test utile protegge una regola osservabile, senza dipendere dai dettagli interni che potresti cambiare durante una riscrittura.
unittest fa parte della libreria standard e non richiede installazioni con pip. La ricerca automatica riconosce normalmente i file test*.py; qui usiamo il prefisso test_ sia per il file sia per i metodi. Le asserzioni di TestCase trasformano una differenza in un test fallito, invece di lasciarla alla lettura manuale di una stampa. La documentazione ufficiale di unittest descrive discovery, asserzioni e sottotest.
Creare ed eseguire i due file completi
In una cartella di esercitazione nuova, salva expense_math.py con questo contenuto:
def total(amounts):
if any(type(x) is not int or x < 0 for x in amounts):
raise ValueError("invalid amount")
return sum(amounts)
Salva test_expense_math.py nella stessa cartella:
import unittest
from expense_math import total
class TotalTests(unittest.TestCase):
def test_regular(self):
self.assertEqual(total([3500, 8000, 2500]), 14000)
def test_empty(self):
self.assertEqual(total([]), 0)
def test_invalid(self):
for values in [[-1], [True], ["8000"]]:
with self.subTest(values=values):
with self.assertRaises(ValueError):
total(values)
if __name__ == "__main__":
unittest.main()
Apri il terminale in quella cartella e avvia l’interprete dell’ambiente che stai usando:
python -m unittest -v
Il riepilogo deve indicare tre test eseguiti e terminare con OK. Il primo controlla 14.000, il secondo zero e il terzo verifica tre ingressi non validi. subTest distingue i singoli casi all’interno dello stesso metodo, perciò non aumenta a cinque il numero dei metodi di test nel riepilogo. Tempi e impaginazione possono variare.
assertEqual confronta risultato ed atteso; assertRaises(ValueError) richiede che l’operazione sollevi proprio quell’eccezione. Non basta controllare che il programma “non si blocchi”. Se non vengono trovati test, controlla il nome del file, la cartella corrente e il prefisso dei metodi prima di interpretare l’esito come una verifica riuscita.
Verificare che i test individuino un difetto
- In
expense_math.py, cambia temporaneamente solo il ritorno inreturn sum(amounts) + 1. Riesegui lo stesso comando: devono fallire sia il caso ordinario sia la lista vuota. - Ripristina
return sum(amounts)e verifica nuovamenteOK. - Rimuovi temporaneamente il controllo
ife il suoraise, lasciando la sola somma. Il test degli ingressi non validi deve segnalare problemi: negativi e booleani non vengono più rifiutati, mentre una stringa può provocare un’eccezione diversa. - Ripristina l’intera funzione originale e riesegui i test. Conserva come versione finale quella che supera tutte le verifiche.
Questa modifica intenzionale, detta mutazione, controlla che il test protegga davvero la regola desiderata. Non dimostra che ogni possibile errore sia coperto: aggiungi nuovi casi quando cambia un requisito o scopri un difetto non rappresentato. Il contratto dell’esempio riguarda liste; non estenderlo automaticamente a generatori, perché una verifica preliminare può consumare un iteratore prima della somma.
Distinguere test unitari e controlli di integrazione
I test precedenti isolano una funzione. Non possono dimostrare che siano corretti un’intestazione CSV, un percorso o una connessione a un database. Un controllo di integrazione mette insieme più parti, per esempio esecuzione del programma, file prodotti e codice di uscita, usando dati e percorsi indipendenti da quelli reali.
Per il controllo seguente serve il file completo csv_report.py del progetto CSV in italiano. Copia da quell’esempio l’intero programma con quel nome nella cartella di esercitazione. Non occorre copiarne il CSV di esempio: il controllo crea un ingresso apposito con due righe dallo stesso identificatore.
Accanto a csv_report.py, salva questo file completo come check_csv.py:
import subprocess
import sys
import tempfile
from pathlib import Path
program = Path("csv_report.py").resolve()
with tempfile.TemporaryDirectory() as folder:
work = Path(folder)
source = work / "expenses-input.csv"
source.write_text("id,category,amount\ne1,food,8000\ne1,food,8000\n", encoding="utf-8")
result = subprocess.run([sys.executable, str(program)], cwd=work, capture_output=True, text=True)
assert result.returncode == 1
assert "food,8000" in (work / "report.csv").read_text(encoding="utf-8")
assert "duplicate id" in (work / "rejected.csv").read_text(encoding="utf-8")
print("CSV integration checks passed")
Esegui python check_csv.py dalla cartella che contiene entrambi i file, con Python senza l’opzione -O: le istruzioni assert di questo piccolo controllo verrebbero eliminate in modalità ottimizzata. L’esito previsto è:
CSV integration checks passed
Il programma CSV viene avviato in una cartella temporanea e deve terminare con codice 1 perché scarta il duplicato. Il rapporto deve contare 8.000 una sola volta e il file degli scarti deve riportare duplicate id. Il processo di controllo, invece, termina normalmente dopo aver confermato tutte e tre le condizioni; la cartella temporanea viene rimossa uscendo dal blocco with.
Se manca csv_report.py o il programma termina diversamente, il controllo fallisce: verifica nome, posizione e contenuto del file senza indebolire l’asserzione per ottenere un risultato positivo. Il percorso del programma viene risolto a partire dalla cartella corrente, mentre i rapporti sono scritti nella cartella temporanea. Il controllo copre questo caso di duplicazione; servono verifiche separate per altri ingressi, guasti di scrittura o database. Quando documenti un’esecuzione, conserva versione dell’interprete, comando, ingressi ed esiti effettivi.




