Testare il codice Python con fixture e monkeypatch in pytest e unittest

by theArchitect
SHARE
Testare il codice Python con fixture e monkeypatch in pytest e unittest
© Guida-HTML5.it

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

SHARE