Immagina di aver appena finito di scrivere una funzione che sembra perfetta. La provi una volta, due, funziona. Il giorno dopo modifichi un’altra parte del codice e… boom, quella funzione smette di funzionare senza che nessuno te lo dica.
È successo a tutti.😅
I test automatici sono esattamente l’antidoto: piccoli pezzi di codice che controllano per te, ogni volta che cambi qualcosa, se il tuo programma continua a comportarsi come deve.
Non è più questione di “sperare che vada bene”.
È questione di sapere, in pochi secondi, se hai rotto qualcosa o no.
Cosa sono i test automatici
I test automatici sono porzioni di codice che verificano il comportamento corretto di altre parti del tuo programma.
Invece di controllare a mano ogni funzione, scrivi uno script che la invoca con input noti e confronta l’output con quello atteso.
Un test automatico tipico fa tre cose:
- Prepara dei dati di input
- Esegue una funzione del tuo codice
- Confronta il risultato con un valore atteso e segnala successo o fallimento
Perché sono fondamentali
| Senza test | Con i test |
|---|---|
| Ogni modifica può introdurre bug silenziosi | Sai subito se hai rotto qualcosa |
| Test manuali ripetitivi e lenti | I test girano in pochi secondi |
| Paura di fare refactoring | Puoi migliorare il codice con sicurezza |
| Bug scoperti solo in produzione | I bug emergono durante lo sviluppo |
I due strumenti principali in Python
unittest (libreria standard)
Incluso in Python, ispirato a JUnit. Richiede classi e nomi di metodi specifici.
pytest (terze parti, il più usato nella community)
Più semplice e potente. Si installa con pip install pytest e permette di scrivere test come normali funzioni.
In questa guida vedremo entrambi, con esempi che puoi copiare e incollare subito su Google Colab.
Esempio pratico: la funzione da testare
Creiamo una funzione semplice ma realistica che calcola l’area di un rettangolo e gestisce gli errori:
# my_math.py
def area_rettangolo(base, altezza):
if base < 0 or altezza < 0:
raise ValueError("Le dimensioni non possono essere negative")
return base * altezza
1. Test con unittest
import unittest
from my_math import area_rettangolo
class TestAreaRettangolo(unittest.TestCase):
def test_area_positiva(self):
self.assertEqual(area_rettangolo(5, 3), 15)
def test_area_con_zero(self):
self.assertEqual(area_rettangolo(0, 10), 0)
def test_valori_negativi(self):
with self.assertRaises(ValueError):
area_rettangolo(-1, 5)
if __name__ == '__main__':
unittest.main()
Come eseguirlo su Google Colab (copia-incolla queste due celle):
%%writefile my_math.py
def area_rettangolo(base, altezza):
if base < 0 or altezza < 0:
raise ValueError("Le dimensioni non possono essere negative")
return base * altezza
import unittest
from my_math import area_rettangolo
class TestAreaRettangolo(unittest.TestCase):
def test_area_positiva(self):
self.assertEqual(area_rettangolo(5, 3), 15)
def test_area_con_zero(self):
self.assertEqual(area_rettangolo(0, 10), 0)
def test_valori_negativi(self):
with self.assertRaises(ValueError):
area_rettangolo(-1, 5)
# Esecuzione nella cella
suite = unittest.TestLoader().loadTestsFromTestCase(TestAreaRettangolo)
unittest.TextTestRunner(verbosity=2).run(suite)
Output atteso:
test_area_con_zero ... ok
test_area_positiva ... ok
test_valori_negativi ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.005s
OK
Guida rapida ai comandi principali di unittest
Da terminale
python -m unittest→ lancia tutti i testpython -m unittest -v→ modalità verbosepython -m unittest discover -s tests→ scopre automaticamente i test in una cartella
Asserzioni più utili
self.assertEqual(a, b)– uguaglianzaself.assertRaises(ValueError)– controlla eccezioniself.assertAlmostEqual– per i floatself.assertIn / assertNotIn– appartenenza
Decoratori utili
@unittest.skip("motivo")@unittest.skipIf(condizione, "motivo")@unittest.expectedFailure(per bug noti)- subTest (per testare tanti casi senza scrivere 10 metodi):
def test_vari_casi(self):
casi = [(5, 3, 15), (0, 10, 0), (10, 0, 0)]
for base, altezza, atteso in casi:
with self.subTest(base=base, altezza=altezza):
self.assertEqual(area_rettangolo(base, altezza), atteso)
2. Test con pytest (il modo più moderno)
Prima installa (su Colab o nel tuo ambiente):
pip install pytest
I test diventano funzioni normali, niente classi:
# test_my_math.py
import pytest
from my_math import area_rettangolo
def test_area_positiva():
assert area_rettangolo(5, 3) == 15
def test_area_con_zero():
assert area_rettangolo(0, 10) == 0
def test_valori_negativi():
with pytest.raises(ValueError):
area_rettangolo(-1, 5)
Versione parametrizzata (equivalente del subTest, ma più pulita):
@pytest.mark.parametrize("base, altezza, atteso", [
(5, 3, 15),
(0, 10, 0),
(10, 0, 0),
(2.5, 4, 10)
])
def test_area_vari_casi(base, altezza, atteso):
assert area_rettangolo(base, altezza) == atteso
Esecuzione
- Terminale:
pytest -v - Colab:
!pytest --verbose
pytest è più veloce da scrivere, produce output più leggibile e ha centinaia di plugin utili (coverage, parametrizzazione avanzata, test paralleli…).
Guida completa e super-dettagliata a pytest per principianti assoluti
Immagina che pytest sia un “controllore automatico” che ogni volta che glielo chiedi verifica se il tuo codice funziona ancora come deve. Non devi più fare prove a mano: scrivi una volta le regole e lui le ripete tutte le volte che vuoi.
Come iniziare in 3 minuti (setup da principiante)
Passo 1 – Installa pytest
Apri il terminale (o la riga di comando) e scrivi esattamente questo:
pip install pytest
Aspetta che finisca. È una sola volta.
Passo 2 – Crea la tua prima cartella di test
Nella cartella del tuo progetto crea una sottocartella chiamata tests (è una convenzione che pytest riconosce automaticamente).
Passo 3 – Crea il tuo primo file di test
Dentro la cartella tests crea un file chiamato test_primo_test.py (deve iniziare con test_ oppure finire con _test.py, altrimenti pytest non lo vede).
Apri il file e incolla questo codice semplicissimo:
# test_primo_test.py
def test_somma_2_piu_2():
assert 2 + 2 == 4
Passo 4 – Esegui il test!
Nel terminale, dalla cartella principale del progetto, scrivi:
pytest
Se tutto va bene vedrai qualcosa del genere:
======================== test session starts ========================
platform ... collected 1 item
tests/test_primo_test.py . [100%]
========================= 1 passed in 0.01s =========================
Il punto . significa “test passato”. Se invece vedi F significa che il test è fallito.
Congratulazioni! Hai appena scritto ed eseguito il tuo primo test automatico.
I comandi principali di pytest (quelli che userai tutti i giorni)
Ecco la tabella dei comandi più utili, spiegati come se avessi 10 anni:
| Comando nel terminale | Cosa significa in parole semplici | Quando lo usi davvero |
|---|---|---|
pytest |
“Esegui tutti i test che trovi” | Uso quotidiano |
pytest -v |
“Esegui tutti i test e dimmi il nome di ognuno” | Quasi sempre |
pytest -q |
“Esegui in silenzio, dimmi solo i punti” | Quando hai 200 test |
pytest nome_file.py |
“Esegui solo i test dentro questo file” | Stai lavorando su uno solo |
pytest -k "somma" |
“Esegui solo i test che contengono ‘somma’” | Filtrare velocemente |
pytest -m lento |
“Esegui solo i test marcati con @pytest.mark.lento” | Test che impiegano tempo |
pytest --tb=no |
“Non mostrarmi l’errore dettagliato” | Vuoi output pulito |
pytest -x |
“Fermati al primo test che fallisce” | Mentre stai correggendo |
pytest --maxfail=3 |
“Fermati dopo 3 test falliti” | Bilanciamento |
pytest --collectonly |
“Mostrami solo quali test troveresti” | Controllo prima di lanciare |
Esempio pratico: Se vuoi eseguire solo i test che parlano di “rettangolo” e vedere tutto in dettaglio:
pytest -v -k "rettangolo"
Come si scrivono i test (le regole d’oro per principianti)
- Ogni test è una funzione normale che inizia con
test_ - Dentro usi la parola magica assert (che in italiano significa “afferma che…”)
- Se l’assert è vero → test passa (punto .)
- Se l’assert è falso → test fallisce (lettera F + spiegazione chiara)
Esempi semplici
def test_area_rettangolo_normale():
assert area_rettangolo(5, 3) == 15 # deve essere esattamente 15
def test_area_con_zero():
assert area_rettangolo(0, 10) == 0
def test_non_accetta_negativi():
with pytest.raises(ValueError): # controlla che venga sollevato un errore
area_rettangolo(-1, 5)
Il trucco più potente per principianti: @pytest.mark.parametrize
Invece di scrivere 10 funzioni uguali, scrivi una sola funzione e gli dai una tabella di valori.
import pytest
from my_math import area_rettangolo
@pytest.mark.parametrize("base, altezza, risultato_atteso", [
(5, 3, 15), # caso normale
(0, 10, 0), # zero
(10, 0, 0), # zero invertito
(2.5, 4, 10.0), # numeri decimali
])
def test_area_rettangolo(base, altezza, risultato_atteso):
assert area_rettangolo(base, altezza) == risultato_atteso
pytest eseguirà automaticamente 4 test diversi, uno per ogni riga della tabella. Se uno solo fallisce, ti dice esattamente quale riga ha sbagliato.
Fixture: il modo per preparare i dati una sola volta
Spesso devi preparare degli oggetti o dei dati prima di ogni test. Invece di ripeterli in ogni funzione, usi una fixture.
@pytest.fixture
def dati_rettangolo():
"""Questa funzione viene eseguita automaticamente prima di ogni test che la richiede"""
return {"base": 10, "altezza": 5}
def test_area_con_fixture(dati_rettangolo):
area = area_rettangolo(dati_rettangolo["base"], dati_rettangolo["altezza"])
assert area == 50
Saltare o marcare test (quando non vuoi eseguirli tutti)
import pytest
import sys
@pytest.mark.skip(reason="Da implementare la prossima settimana")
def test_funzione_futura():
...
@pytest.mark.skipif(sys.version_info < (3, 11), reason="Serve Python 3.11 o superiore")
def test_feature_nuova():
...
@pytest.mark.xfail(reason="Bug conosciuto #45") # come "expectedFailure" di unittest
def test_che_sappiamo_che_fallisce():
...
Struttura di cartelle consigliata (per non perdersi)
il_mio_progetto/
├── my_math.py ← il codice da testare
├── tests/
│ ├── __init__.py ← file vuoto, serve solo per Python
│ ├── test_my_math.py ← tutti i test della funzione area
│ └── test_altre_cose.py
└── conftest.py ← (opzionale) fixture condivise tra tutti i test
Riassunto finale da tenere sul desktop
- Installa una volta:
pip install pytest - File di test devono iniziare con
test_ - Usa assert normale (non
self.assertQualcosa) - Per tanti casi usa @pytest.mark.parametrize
- Per preparare dati usa @pytest.fixture
- Comando più utile:
pytest -v
Provalo subito
Copia il codice del rettangolo che abbiamo visto nell’articolo precedente, crea il file test_my_math.py e lancia pytest -v.
Confronto veloce unittest vs pytest
| Aspetto | unittest | pytest |
|---|---|---|
| Installazione | già presente | pip install pytest |
| Scrittura test | classi e metodi obbligatori | funzioni normali |
| Assert | self.assertEqual | assert normale |
| Parametrizzazione | subTest | @pytest.mark.parametrize |
| Lettura output | buona | eccellente e colorata |
| Popolarità community | buona | di gran lunga la più usata |
Perché abbiamo scelto l’esercizio del rettangolo
Anche se calcolare l’area di un rettangolo sembra una cosa banalissima (basta fare [math]\text{base} \cdot \text{altezza}[/math]), in realtà è un esempio perfetto per imparare a scrivere test.
È come un piccolo “laboratorio completo” che contiene tutto quello che serve nella vita reale, ma senza complicazioni inutili.
Immagina di avere una funzione che deve funzionare sempre bene: non deve mai dare risultati sbagliati, deve segnalare subito gli errori e deve essere facile da capire per chi legge il codice dopo di te.
L’esercizio del rettangolo ti insegna esattamente questo, in modo semplice e diretto.
Ecco i motivi, spiegati uno per uno come se fossi al tuo fianco:
1. È completamente isolato (nessuna dipendenza esterna)
La funzione area_rettangolo non chiama database, non fa richieste a internet, non legge file. Prende due numeri e restituisce un risultato.
Perché è importante?
Il test è deterministico: se dai sempre gli stessi numeri (es. 5 e 3), ottieni sempre lo stesso risultato (15). Non cambia mai. Questo è il concetto base di un unit test puro (test di una singola funzione).
Ti insegna la differenza tra:
- Unit test → testiamo solo questa piccola funzione (come qui)
- Test di integrazione → quando la funzione parla con altre parti del programma (database, API, ecc.)
Imparando con il rettangolo, capisci subito la differenza senza confusione.
2. Ti costringe a pensare ai diversi “tipi di dati”
Quando si testa, non basta provare un paio di numeri a caso. Devi pensare a tutte le situazioni possibili. L’esercizio del rettangolo ti obbliga a farlo in modo naturale:
- Happy Path (il caso normale, quello che dovrebbe succedere sempre)
- Valori positivi: [math]5 \cdot 3 = 15[/math]
- Anche con i decimali: [math]2.5 \cdot 4 = 10[/math]
- Boundary Values (i valori al confine, quelli più pericolosi)
- Lo zero è il limite esatto: [math]\text{base}=0[/math] o [math]\text{altezza}=0 \rightarrow \text{area}=0[/math]
- Molti bug nascono proprio qui (per esempio in altri calcoli successivi potrebbe capitare una divisione per zero).
- Error Path (i casi di errore)
- Numeri negativi: [math]\text{base}=-1, \text{altezza}=5[/math]
- In geometria un’area negativa non esiste.
- La funzione deve fallire subito (Fail Fast) sollevando un errore chiaro (
ValueError) invece di restituire un numero negativo come [math]-5 \cdot 3 = -15[/math].
Questo insegna una regola d’oro: meglio dire “questo input è sbagliato” subito, piuttosto che continuare con un risultato matematicamente corretto ma logicamente senza senso.
Questi tre tipi di casi (normale, confine, errore) si chiamano in gergo tecnico Equivalence Partitioning e Boundary Value Analysis.
Con il rettangolo li impari senza neanche accorgertene.
3. I test diventano una “documentazione viva” del contratto della funzione
Guarda questo pezzetto di test:
def test_valori_negativi():
with pytest.raises(ValueError):
area_rettangolo(-1, 5)
Chiunque legga questo test (anche un collega che non ha mai visto il file my_math.py) capisce immediatamente:
- La funzione non accetta numeri negativi
- Se li riceve, deve lanciare un errore preciso
Non serve leggere il codice sorgente per sapere come deve comportarsi la funzione. I test spiegano il contratto (le regole) della funzione meglio di tanti commenti.
In sintesi: perché è perfetto per un principiante
- È semplice → non ti perdi in dettagli complicati
- È completo → contiene già tutti i concetti importanti del testing
- È realistico → nella vita reale fai esattamente queste cose con funzioni molto più grandi (calcolo prezzi, validazione dati, ecc.)
- Ti fa capire subito il “perché” dei test, non solo il “come”
Una volta capito l’esercizio del rettangolo, passare a testare funzioni vere (calcolo spedizioni in un e-commerce, pulitura di dati, validazione di un form) diventa naturale: applichi esattamente gli stessi ragionamenti.
È come imparare a guidare con una macchina molto semplice: una volta presa la mano, puoi guidare qualsiasi auto senza problemi. Questo è il motivo per cui lo usiamo sempre come primo esempio: ti dà le fondamenta solide senza farti sentire sopraffatto.
Dal tutorial alla produzione: perché i test ti salvano la carriera
Calcolare l’area di un rettangolo sembra una cosa banale, ma è un esempio perfetto proprio perché contiene tutto quello che serve nella pratica:
- Casi normali (valori positivi)
- Casi limite (zero)
- Casi di errore (valori negativi)
Nella vita reale è esattamente quello che fai con funzioni critiche: validi i dati in ingresso, gestisci gli edge case e impedisci che un valore sbagliato mandi in crash l’applicazione (pensa a un sistema di preventivi, a un software di progettazione o a un e-commerce che calcola dimensioni di imballaggi).
Inoltre l’esercizio ti insegna due abitudini d’oro:
- I test non servono solo a “verificare che funzioni”, ma anche a documentare il comportamento atteso. Chi legge i test capisce subito che le dimensioni negative sono proibite.
- Usare subTest o @pytest.mark.parametrize è la strada per non scrivere 10 metodi identici quando devi testare tante combinazioni.
Chi capisce questo piccolo esempio ha già in mano la logica per testare funzioni molto più complesse.
E-commerce (il classico incubo del Black Friday)
Luca lavora in un negozio online di abbigliamento. Ha una funzione [math]\text{calcola\_spedizione}(\text{peso, provincia})[/math] che decide il costo di spedizione.
Un venerdì pomeriggio modifica il codice per aggiungere un nuovo corriere. Sembra una cosa da niente.
Il lunedì mattina arrivano centinaia di ordini… e il sistema addebita 0 € di spedizione a tutti i clienti della Sicilia.
Risultato: perdita di migliaia di euro e clienti arrabbiati.
Con pytest (e un semplice
@pytest.mark.parametrizecon 20 combinazioni di peso/provincia) il bug sarebbe stato scoperto in locale, prima ancora di fare commit. Luca oggi dice: “Da quel giorno ogni modifica passa prima dai test. Non voglio più vivere quel panico.”
Data pipeline (quando i dati “sporchi” rovinano tutto)
Sara è data analyst in una startup di marketing. Ha scritto una funzione pulisci_email(lista_email) che valida e normalizza gli indirizzi prima di caricarli nel database.
Durante un refactor ha tolto per sbaglio il controllo sui domini internazionali (tipo .io o .app).
Risultato: nel database sono finiti migliaia di record invalidi, le campagne email hanno iniziato a rimbalzare e il team ha perso due giorni per pulire tutto.
Con pytest e una fixture che prepara 50 email di test (buone, cattive, con caratteri strani) Sara ora vede subito se la funzione si rompe. La pipeline gira ogni notte in CI/CD e lei dorme sonni tranquilli.
Queste non sono storie inventate: sono cose che succedono ogni giorno in aziende vere. pytest non è solo “codice in più”: è la differenza tra “ops, ho rotto tutto” e “tutto sotto controllo”.
❗ Il bagno di realtà: Quando i test automatici NON bastano
Per quanto pytest sia un’arma potentissima, c’è una trappola in cui cadono molti sviluppatori alle prime armi: credere al mito che “test tutti verdi = software perfetto”.
I test automatici confermano solo che il codice fa esattamente quello che gli hai detto di fare. Ma cosa succede se gli hai detto di fare la cosa sbagliata? Ecco tre scenari in cui i test non possono salvarti:
-
Non sostituiscono l’occhio umano e la UX (User Experience): Un test automatico può certificare che la funzione
aggiungi_al_carrello()scatta in 0.01 secondi e aggiorna il database. Ma non ti dirà mai che sul sito web il bottone “Acquista” è illeggibile su mobile, coperto da un banner o frustrante da cliccare. I test verificano la logica, gli umani verificano l’esperienza. -
Non trovano i bug di “Design” o i requisiti fraintesi: Se il cliente ti ha chiesto un sistema per calcolare l’IVA al 22% e tu scrivi una logica perfetta che la calcola al 10%, i tuoi test passeranno tutti con orgoglio. Hai scritto un codice impeccabile per il problema sbagliato. I test validano che l’ingranaggio giri bene, ma non sanno dirti se fa parte del motore giusto.
-
Non evitano i disastri se il test stesso contiene errori di logica: È il classico principio del garbage in, garbage out (spazzatura dentro, spazzatura fuori). Se chi scrive il test non ha capito le regole di business e imposta un’asserzione errata (es.
assert calcola_spedizione(peso) == 0quando invece si doveva pagare), il test diventerà “verde” solo quando il codice nasconderà un buco economico per l’azienda. Un test sbagliato è infinitamente peggio di nessun test, perché ti regala una letale e falsa sensazione di sicurezza.
La regola d’oro del vero Senior: Usa i test automatici per blindare la logica del codice, le formule e i casi limite, in modo da non doverci pensare mai più. Ma usa sempre il buon senso (e il testing manuale) per valutare l’usabilità e la direzione generale del prodotto.
Il livello successivo: i “Mock” (ovvero le controfigure del tuo codice)
Finora abbiamo testato una funzione matematica che vive in isolamento. Ma cosa succede se la tua funzione deve interagire con il mondo esterno, ad esempio scaricare dati da un’API, scrivere su un database o inviare un’email al cliente?
Non vuoi assolutamente che il tuo test invii davvero un’email ogni volta che premi invio. Inoltre, se manca la connessione a internet, il test fallirebbe anche se il tuo codice è perfetto.
Qui entra in gioco il Mocking.
Pensa al Mock come alla controfigura di un film d’azione: prende il posto dell’attore reale nelle scene pericolose o complicate. In Python (usando la libreria integrata unittest.mock o l’ottimo plugin pytest-mock), puoi dire al tuo test: “Ehi, quando arrivi alla riga in cui devi inviare l’email, fai finta di averla inviata con successo e vai avanti”.
Esempio pratico con pytest
Ecco come si presenta in pytest usando la fixture magica mocker:
# Il nostro codice originale
def invia_email_benvenuto(email):
# Immagina del codice lento che contatta un server reale...
pass
def registra_utente(email):
# ... logica di registrazione ...
invia_email_benvenuto(email)
return True
# Il nostro test
def test_registra_utente_senza_inviare_spam(mocker):
# Blocchiamo la funzione reale e ci mettiamo una "controfigura"
mocker.patch("mio_progetto.invia_email_benvenuto")
# Eseguiamo la funzione
risultato = registra_utente("[email protected]")
# Il test passa, ci mette un millisecondo e nessuno riceve spam!
assert risultato == True
Il Mocking è uno scoglio su cui molti principianti sbattono la testa, ma capirne l’utilità fin dall’inizio ti risparmierà ore di frustrazione. Quando i tuoi test inizieranno a diventare lenti o a fallire per colpa della rete, saprai che è il momento di chiamare una controfigura.





