Introduzione
Quando si parla di test e debugging in Python, uno degli aspetti più utili per lavorare in modo professionale è imparare a isolare il codice che vuoi verificare. In pratica, non basta controllare che una funzione restituisca il risultato corretto: spesso devi anche simulare dipendenze esterne, come lettura di file, accesso a variabili d’ambiente, chiamate a servizi o funzioni di sistema.
In questo tutorial vediamo un sotto-argomento molto pratico e spesso sottovalutato: l’uso di fixture e monkeypatch per testare codice con dipendenze esterne. Lo faremo con pytest, ma mostreremo anche come ottenere un comportamento simile con unittest usando unittest.mock.
Questo approccio è fondamentale perché rende i test:
- deterministici, quindi meno soggetti a errori casuali;
- veloci, perché eviti accessi reali a risorse lente;
- facili da debug-are, perché ogni dipendenza è controllata;
- più leggibili, se organizzi bene fixture e mock.
Codice completo
Nel nostro esempio costruiamo un piccolo modulo che legge un valore da una variabile d’ambiente e calcola uno sconto. Poi lo testiamo in due modi: con pytest usando fixture e monkeypatch, e con unittest usando patch.
# file: pricing.py
import os
def get_discounted_price(base_price: float) -> float:
"""
Restituisce il prezzo finale applicando uno sconto percentuale
letto dalla variabile d´ambiente DISCOUNT_PERCENT.
Se la variabile non esiste, usa 0.
"""
percent_str = os.getenv("DISCOUNT_PERCENT", "0")
discount_percent = float(percent_str)
return base_price * (1 - discount_percent / 100)
def normalize_username(username: str) -> str:
"""
Normalizza un nome utente:
- rimuove spazi iniziali/finali
- converte in minuscolo
- sostituisce gli spazi interni con underscore
"""
return username.strip().lower().replace(" ", "_")
# file: test_pricing_pytest.py
import os
import pytest
from pricing import get_discounted_price, normalize_username
@pytest.fixture
def discount_20(monkeypatch):
# Imposta una variabile d´ambiente temporanea solo per il test
monkeypatch.setenv("DISCOUNT_PERCENT", "20")
return os.getenv("DISCOUNT_PERCENT")
def test_discounted_price_with_fixture(discount_20):
price = get_discounted_price(100)
assert price == 80.0
def test_discounted_price_without_env(monkeypatch):
# Rimuove la variabile d´ambiente per simulare il caso "default"
monkeypatch.delenv("DISCOUNT_PERCENT", raising=False)
price = get_discounted_price(50)
assert price == 50.0
def test_normalize_username():
assert normalize_username(" Mario Rossi ") == "mario_rossi"
def test_normalize_username_with_parametrization():
cases = [
(" Alice ", "alice"),
("John Doe", "john_doe"),
(" Python Dev ", "python___dev"),
]
for input_value, expected in cases:
assert normalize_username(input_value) == expected
# file: test_pricing_unittest.py
import unittest
from unittest.mock import patch
from pricing import get_discounted_price, normalize_username
class TestPricing(unittest.TestCase):
@patch("pricing.os.getenv", return_value="25")
def test_discounted_price_with_patch(self, mock_getenv):
price = get_discounted_price(200)
self.assertEqual(price, 150.0)
mock_getenv.assert_called_once_with("DISCOUNT_PERCENT", "0")
@patch("pricing.os.getenv", return_value="0")
def test_discounted_price_default(self, mock_getenv):
price = get_discounted_price(80)
self.assertEqual(price, 80.0)
def test_normalize_username(self):
self.assertEqual(normalize_username(" Ada Lovelace "), "ada_lovelace")
if __name__ == "__main__":
unittest.main()
Spiegazione
Il file pricing.py contiene due funzioni semplici ma realistiche. La prima, get_discounted_price, dipende da una variabile d’ambiente. Questa è una dipendenza esterna: il risultato della funzione cambia in base all’ambiente in cui gira. La seconda, normalize_username, è pura e non dipende da nulla fuori dalla funzione stessa.
Nel test con pytest, la fixture discount_20 usa monkeypatch.setenv per impostare una variabile d’ambiente temporanea. Il vantaggio è che il test non modifica davvero l’ambiente del sistema: la modifica esiste solo durante l’esecuzione del test. Questo è perfetto per evitare effetti collaterali.
Nel test test_discounted_price_without_env, usiamo monkeypatch.delenv per simulare l’assenza della variabile. Così verifichiamo il comportamento di default della funzione, un caso importante in ogni suite di test ben progettata.
La funzione normalize_username è un ottimo esempio di codice semplice da testare: non richiede mock, fixture o patch. Questo ci ricorda una regola importante: più il codice è puro, più è facile da testare.
Nel file unittest, usiamo patch("pricing.os.getenv", return_value="25") per sostituire temporaneamente la funzione os.getenv. È importante patchare il simbolo nel namespace corretto: qui patchiamo pricing.os.getenv, non os.getenv in generale, perché la funzione sotto test importa e usa quel riferimento nel modulo pricing.
La riga mock_getenv.assert_called_once_with("DISCOUNT_PERCENT", "0") dimostra anche come verificare non solo il risultato finale, ma pure l’interazione con la dipendenza. Questo è utile nel debugging: se un test fallisce, puoi capire se il problema è nel calcolo o nel modo in cui la funzione accede alla dipendenza.
Best practice
- Preferisci funzioni pure quando possibile: sono più facili da testare e da mantenere.
- Isola le dipendenze esterne: variabili d’ambiente, file, rete e database dovrebbero essere controllati nei test.
- Usa fixture per setup ripetuti: in pytest le fixture rendono il codice più pulito rispetto a duplicare setup in ogni test.
- Usa monkeypatch per modifiche temporanee: è ideale per ambiente, attributi di modulo e configurazioni locali al test.
- Patcha nel punto giusto: con unittest.mock devi sostituire il simbolo nel modulo dove viene usato, non necessariamente dove è definito.
- Testa anche i casi di default e di errore: un bug comune nasce proprio dai valori mancanti o non validi.
- Evita test troppo accoppiati all’implementazione: verifica il comportamento, non ogni dettaglio interno, salvo quando serve davvero.
Riepilogo
In questo tutorial abbiamo visto come testare codice Python che dipende da risorse esterne usando fixture e monkeypatch in pytest, e come ottenere un risultato simile con unittest.mock.patch in unittest.
Il punto chiave è semplice: quando il codice dipende dall’esterno, i test devono controllare quell’esterno in modo artificiale e ripetibile. Così ottieni test più affidabili, più facili da eseguire in CI/CD e più utili durante il debugging.
Se vuoi migliorare la qualità dei tuoi test, il prossimo passo naturale è imparare a combinare questi strumenti con parametrizzazione, test delle eccezioni e organizzazione delle suite di test.
Approfondisci con risorse ufficiali
- pytest documentation: https://docs.pytest.org/
- pytest monkeypatch: https://docs.pytest.org/en/stable/how-to/monkeypatch.html
- unittest.mock: https://docs.python.org/3/library/unittest.mock.html
- unittest: https://docs.python.org/3/library/unittest.html
- os.getenv: https://docs.python.org/3/library/os.html#os.getenv
