Test delle eccezioni in Python metodi puliti ed efficaci
Test delle eccezioni in Python
Oltre i Fondamenti: Test Avanzati sulle Eccezioni in Python per Pytest e Unittest

Testare le eccezioni è più di una formalità: è una parte essenziale nella scrittura di codice affidabile. In questo tutorial, esploreremo metodi pratici ed efficaci per testare il codice Python che solleva e non solleva eccezioni, verificando l’accuratezza dei messaggi di eccezione e coprendo sia pytest
che unittest
, con e senza test parametrizzati per ciascun framework.
Alla fine di questo tutorial, avrai una solida comprensione di come scrivere test di eccezione puliti, efficienti e informativi per il tuo codice.
Analizziamo l’esempio seguente:
def divide(num_1: float, num_2: float) -> float: if not isinstance(num_1, (int, float)) \ or not isinstance(num_2, (int, float)): raise TypeError("almeno uno dei valori in input " f"non è un numero: {num_1}, {num_2}") return num_1 / num_2
Ci sono diversi casi che possiamo testare per la funzione sopra – un flusso regolare, un denominatore zero e un input non numerico.
- Introducendo Agents.js Dai strumenti ai tuoi LLMs utilizzando JavaScript
- Machine Unlearning nel 2023 Dove siamo e dove stiamo andando
- IA che insegna ad altre IA
Ora vediamo come apparirebbero tali test, utilizzando pytest
:
pytest
from contextlib import nullcontext as does_not_raiseimport pytestfrom operations import dividedef test_flusso_regolare(): with does_not_raise(): assert divide(30, 2.5) is not None assert divide(30, 2.5) == 12.0def test_divisione_per_zero(): with pytest.raises(ZeroDivisionError) as exc_info: divide(10.5, 0) assert exc_info.value.args[0] == "float division by zero"def test_non_numero(): with pytest.raises(TypeError) as exc_info: divide("a", 10.5) assert exc_info.value.args[0] == \ "almeno uno dei valori in input non è un numero: a, 10.5"
Possiamo anche eseguire un controllo per vedere cosa succede quando testiamo un flusso non valido rispetto al tipo di eccezione sbagliato o quando cerchiamo di verificare una eccezione sollevata in un flusso regolare. In questi casi, i test falliranno:
# Entrambi i test sottostanti dovrebbero falliredef test_eccezione_errata(): with pytest.raises(TypeError) as exc_info: divide(10.5, 0) assert exc_info.value.args[0] == "float division by zero"def test_eccezione_inaspettata_in_flusso_regolare(): with pytest.raises(Exception): assert divide(30, 2.5) is not None
Quindi, perché i test sopra sono falliti? Il contesto with
cattura il tipo specifico di eccezione richiesto e verifica che il tipo di eccezione sia effettivamente quello richiesto.
In test_eccezione_errata
, un’eccezione (ZeroDivisionError
) è stata generata, ma non è stata catturata da TypeError.
Pertanto, nello stack trace, vedremo che ZeroDivisionError
è stata generata e non è stata catturata dal contesto TypeError
.
In test_eccezione_inaspettata_in_flusso_regolare
il nostro contesto with pytest.raises
ha cercato di convalidare il tipo di eccezione richiesto (abbiamo fornito Exception
in questo caso), ma poiché nessuna eccezione è stata generata, il test è fallito con il messaggio Failed: DID NOT RAISE <class ‘Exception’>
.
Passando ora alla fase successiva, esploriamo come rendere i nostri test molto più concisi e puliti utilizzando parametrize
.
Parametrize
from contextlib import nullcontext as does_not_raiseimport pytestfrom operations import [email protected]( "num_1, num_2, expected_result, exception, message", [ (30, 2.5, 12.0, does_not_raise(), None), (10.5, 0, None, pytest.raises(ZeroDivisionError), "float division by zero"), ("a", 10.5, None, pytest.raises(TypeError), "almeno uno dei valori in input non è un numero: a, 10.5") ], ids=["input validi", "divisione per zero", "input non numerico"])def test_divisione(num_1, num_2, expected_result, exception, message): with exception as e: result = divide(num_1, num_2) assert message is None or message in str(e) if expected_result is not None: assert result == expected_result
Il parametro ids
cambia il nome del caso di test visualizzato nella vista della barra di test dell’IDE. Nella schermata sottostante possiamo vederlo in azione: con ids
a sinistra e senza ids
a destra.

Ora che abbiamo coperto il framework pytest
, vediamo come scrivere gli stessi test utilizzando unittest
.
unittest
from unittest import TestCasefrom operations import divideclass TestDivide(TestCase): def test_happy_flow(self): result = divide(0, 10.5) self.assertEqual(result, 0) def test_division_by_zero(self): with self.assertRaises(ZeroDivisionError) as context: divide(10, 0) self.assertEqual(context.exception.args[0], "divisione per zero") def test_not_a_digit(self): with self.assertRaises(TypeError) as context: divide(10, "c") self.assertEqual(context.exception.args[0], "almeno uno dei valori di input " "non è un numero: 10, c")
Se vogliamo utilizzare parameterized
con unittest
dobbiamo installare il pacchetto. Vediamo come apparirebbero i test parametrizzati in unittest
:
Parametrized
import unittestfrom parameterized import parameterized # richiede l'installazionefrom operations import dividedef get_test_case_name(testcase_func, _, param): test_name = param.args[-1] return f"{testcase_func.__name__}_{test_name}"class TestDivision(unittest.TestCase): @parameterized.expand([ (30, 2.5, 12.0, None, None, "input validi"), (10.5, 0, None, ZeroDivisionError, "divisione in virgola mobile per zero", "divisione per zero"), ("a", 10.5, None, TypeError, "almeno uno dei valori di input non è un numero: a, 10.5", "input non numerico") ], name_func=get_test_case_name) def test_division(self, num_1, num_2, expected_result, exception_type, exception_message, test_name): with self.subTest(num_1=num_1, num_2=num_2): if exception_type is not None: with self.assertRaises(exception_type) as e: divide(num_1, num_2) self.assertEqual(str(e.exception), exception_message) else: result = divide(num_1, num_2) self.assertIsNotNone(result) self.assertEqual(result, expected_result)
In unittest
, abbiamo anche modificato i nomi dei casi di test, simili all’esempio di pytest
sopra. Tuttavia, per ottenere questo, abbiamo utilizzato il parametro name_func
insieme a una funzione personalizzata.
In conclusione, oggi abbiamo esplorato metodi efficaci per testare le eccezioni Python. Abbiamo imparato a riconoscere quando viene lanciata un’eccezione come previsto e a verificare che il messaggio dell’eccezione corrisponda alle nostre aspettative. Abbiamo esaminato vari modi per testare una funzione divide
, inclusa l’approccio tradizionale utilizzando pytest
e un approccio più pulito con parametrize
. Abbiamo anche esplorato l’equivalente di unittest
con parameterized
, che richiedeva l’installazione della libreria, così come senza di essa. L’uso di ids
e nomi di test personalizzati ha fornito una visualizzazione più pulita e informativa nella barra dei test dell’IDE, facilitando la comprensione e la navigazione dei casi di test. Utilizzando queste tecniche, possiamo migliorare i nostri test unitari e assicurarci che il nostro codice gestisca correttamente le eccezioni.
Buon testing!
