Introduzione
Un buon test non verifica soltanto che il percorso principale di una funzione funzioni correttamente. Deve controllare anche i casi limite, gli input non validi e le situazioni che spesso causano errori difficili da individuare. In questo tutorial costruiremo una piccola applicazione Python per calcolare il prezzo finale di un ordine, applicando sconti e costi di spedizione.
Impareremo a:
- organizzare il codice in modo facilmente testabile;
- scrivere test con unittest;
- scrivere test equivalenti con pytest;
- verificare i casi limite, come valori nulli, negativi o soglie precise;
- misurare quali righe del codice vengono realmente eseguite usando pytest-cov;
- usare i messaggi di errore per diagnosticare rapidamente un test fallito.
La copertura del codice, chiamata comunemente code coverage, è un indicatore utile, ma non dimostra da sola che il software sia corretto. Una copertura elevata con test poco significativi può comunque lasciare molti bug.
Codice completo
Creiamo un file chiamato ordine.py. La funzione applica uno sconto in base al totale degli articoli e aggiunge la spedizione soltanto quando il totale dopo lo sconto è inferiore a 50 euro.
from decimal import Decimal
SOGLIA_SCONTO = Decimal("100.00")
PERCENTUALE_SCONTO = Decimal("0.10")
SOGLIA_SPEDIZIONE_GRATUITA = Decimal("50.00")
COSTO_SPEDIZIONE = Decimal("6.90")
def calcola_totale(prezzo_unitario, quantita):
"""Calcola il totale finale di un ordine."""
prezzo = Decimal(str(prezzo_unitario))
if quantita <= 0:
raise ValueError("La quantità deve essere positiva")
if prezzo < 0:
raise ValueError("Il prezzo non può essere negativo")
subtotale = prezzo * quantita
if subtotale >= SOGLIA_SCONTO:
sconto = subtotale * PERCENTUALE_SCONTO
else:
sconto = Decimal("0.00")
totale_scontato = subtotale - sconto
if totale_scontato < SOGLIA_SPEDIZIONE_GRATUITA:
totale_scontato += COSTO_SPEDIZIONE
return totale_scontato.quantize(Decimal("0.01"))
def formatta_euro(importo):
"""Restituisce un importo formattato in euro."""
return f"{importo:.2f} €" Ora creiamo una suite con unittest nel file test_ordine_unittest.py.
import unittest
from decimal import Decimal
from ordine import calcola_totale, formatta_euro
class TestCalcolaTotale(unittest.TestCase):
def test_ordine_sotto_la_soglia_di_sconto(self):
risultato = calcola_totale("20.00", 2)
self.assertEqual(risultato, Decimal("46.90"))
def test_ordine_esattamente_sulla_soglia_di_sconto(self):
risultato = calcola_totale("100.00", 1)
self.assertEqual(risultato, Decimal("90.00"))
def test_spedizione_gratuita_dopo_lo_sconto(self):
risultato = calcola_totale("60.00", 1)
self.assertEqual(risultato, Decimal("60.00"))
def test_quantita_non_valida(self):
with self.assertRaises(ValueError):
calcola_totale("10.00", 0)
def test_prezzo_negativo(self):
with self.assertRaises(ValueError):
calcola_totale("-5.00", 1)
def test_formattazione_euro(self):
self.assertEqual(formatta_euro(Decimal("12.50")), "12.50 €")
if __name__ == "__main__":
unittest.main() La stessa logica può essere espressa in modo più compatto con pytest. Salviamo il file come test_ordine_pytest.py.
import pytest
from decimal import Decimal
from ordine import calcola_totale, formatta_euro
def test_ordine_sotto_la_soglia_di_sconto():
assert calcola_totale("20.00", 2) == Decimal("46.90")
def test_ordine_esattamente_sulla_soglia_di_sconto():
assert calcola_totale("100.00", 1) == Decimal("90.00")
def test_spedizione_gratuita_dopo_lo_sconto():
assert calcola_totale("60.00", 1) == Decimal("60.00")
def test_quantita_non_valida():
with pytest.raises(ValueError, match="quantità"):
calcola_totale("10.00", 0)
def test_prezzo_negativo():
with pytest.raises(ValueError, match="negativo"):
calcola_totale("-5.00", 1)
def test_formattazione_euro():
assert formatta_euro(Decimal("12.50")) == "12.50 €" Spiegazione
I test controllano principalmente le transizioni di soglia. Un errore frequente consiste nell´usare per sbaglio > al posto di >=. Per questo testiamo esplicitamente un ordine da 100 euro: è il primo valore che deve ottenere lo sconto.
Testiamo anche la spedizione gratuita dopo l´applicazione dello sconto. Un ordine da 60 euro diventa 54 euro dopo lo sconto e quindi supera la soglia di spedizione gratuita. Questo controllo impedisce di calcolare la spedizione sul subtotale originale quando la regola di business richiede di usare il totale scontato.
Il comando per eseguire i test con unittest è:
python -m unittest discover -v Per eseguire i test con pytest installiamo prima il pacchetto:
python -m pip install pytest
pytest -v Per misurare la copertura installiamo il plugin pytest-cov:
python -m pip install pytest-cov
pytest --cov=ordine --cov-report=term-missing -v L´opzione --cov-report=term-missing indica anche le righe non eseguite dai test. Se, per esempio, manca un test per un particolare ramo condizionale, il report mostrerà il numero della riga da analizzare.
Durante il debugging è utile eseguire un solo test:
pytest test_ordine_pytest.py::test_ordine_esattamente_sulla_soglia_di_sconto -vv La modalità -vv produce informazioni più dettagliate. Se un´asserzione fallisce, pytest mostra normalmente il valore atteso e quello ricevuto, rendendo più semplice individuare la differenza.
Best practice
- Testa le regole, non l´implementazione interna. Verifica il risultato osservabile invece di controllare ogni variabile locale.
- Copri entrambi i lati delle soglie. Se una regola usa 100 come limite, prova valori inferiori, uguali e superiori.
- Usa nomi descrittivi. Un nome come
test_ordine_esattamente_sulla_soglia_di_scontodocumenta già lo scopo del test. - Preferisci importi monetari precisi.
Decimalevita molti problemi dovuti alla rappresentazione binaria dei numeri in virgola mobile. - Non inseguire il 100% a ogni costo. La coverage è uno strumento diagnostico: la qualità delle asserzioni è più importante della percentuale.
- Controlla i messaggi delle eccezioni quando sono parte del contratto. In pytest,
matchconsente di verificare anche il testo dell´errore. - Isola i test. Ogni test deve poter essere eseguito da solo e non deve dipendere dall´ordine degli altri test.
Riepilogo
Abbiamo creato una funzione per il calcolo di un ordine e l´abbiamo verificata con sia unittest sia pytest. I test hanno coperto il percorso normale, le soglie di sconto, la spedizione gratuita, gli input non validi e la formattazione.
Abbiamo inoltre usato pytest-cov per capire quali righe vengono eseguite. Un approccio efficace consiste nel partire dai requisiti, individuare le condizioni limite e scrivere test piccoli, leggibili e indipendenti. Quando un test fallisce, eseguirlo singolarmente con informazioni dettagliate permette di ridurre rapidamente il problema a una specifica regola o istruzione.
