Testing Unitario in Python: Guida Completa con Esempi
Test Python con pytest, TDD, mocking e coverage
L’unità di test garantisce che il tuo codice Python funzioni correttamente e continui a funzionare mentre il progetto evolve. Questa guida completa copre tutto ciò che devi sapere sulle unità di test in Python, dagli concetti di base alle tecniche avanzate.

Perché le unità di test contano
Le unità di test offrono numerosi vantaggi per gli sviluppatori Python:
- Rilevamento precoce degli errori: Cattura gli errori prima che arrivino in produzione
- Qualità del codice: Ti obbliga a scrivere codice modulare e testabile
- Confidenza nel refactoring: Modifica in modo sicuro sapendo che i test rileveranno le regressioni
- Documentazione: I test servono come documentazione eseguibile su come dovrebbe funzionare il codice
- Sviluppo più rapido: I test automatici sono più veloci dei test manuali
- Progettazione migliore: Scrivere codice testabile porta a una migliore architettura
Comprendere i fondamenti delle unità di test
Cosa è un test unitario?
Un test unitario verifica la parte più piccola e testabile di un’applicazione (di solito una funzione o un metodo) in isolamento. Dovrebbe essere:
- Veloce: Eseguito in millisecondi
- Isolato: Indipendente da altri test e sistemi esterni
- Ripetibile: Produca gli stessi risultati ogni volta
- Autoverificante: Passi o fallisca chiaramente senza ispezione manuale
- Tempestivo: Scritto prima o insieme al codice
La piramide dei test
Un insieme di test sano segue la piramide dei test:
/\
/ \ Test E2E (Pochi)
/____\
/ \ Test di integrazione (Alcuni)
/_____ ___\
/ \ Test unitari (Molti)
/_____ __ ___\
I test unitari formano la base - sono numerosi, veloci e forniscono un feedback rapido.
Confronto tra framework di test per Python
unittest: Il framework integrato
La libreria standard di Python include unittest, ispirato a JUnit:
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Eseguito prima di ogni test"""
self.calc = Calculator()
def tearDown(self):
"""Eseguito dopo ogni test"""
self.calc = None
def test_addition(self):
result = self.calc.add(2, 3)
self.assertEqual(result, 5)
def test_division_by_zero(self):
with self.assertRaises(ZeroDivisionError):
self.calc.divide(10, 0)
if __name__ == '__main__':
unittest.main()
Punti di forza:
- Integrato, non necessita di installazione
- Buono per gli sviluppatori familiari con i framework xUnit
- Amichevole per le aziende, ben stabilito
Punti deboli:
- Sintassi verbosa con codice boilerplate
- Richiede classi per l’organizzazione dei test
- Gestione dei fixture meno flessibile
pytest: La scelta moderna
pytest è il framework di test terze parti più popolare:
import pytest
def test_addition():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_division_by_zero():
calc = Calculator()
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_addition_parametrized(a, b, expected):
calc = Calculator()
assert calc.add(a, b) == expected
Punti di forza:
- Sintassi semplice e Pythonica
- Sistema di fixture potente
- Ecosistema di plugin eccellente
- Segnalazione degli errori migliore
- Test parametrizzati integrati
Punti deboli:
- Richiede l’installazione
- Meno familiare agli sviluppatori provenienti da altri linguaggi
Installazione:
pip install pytest pytest-cov pytest-mock
Scrivere i primi test unitari
Costruiamo un esempio semplice da zero utilizzando lo sviluppo guidato dai test (TDD). Se sei nuovo a Python o hai bisogno di un riferimento rapido per la sintassi e le caratteristiche del linguaggio, consulta il nostro Python Cheatsheet per un’overview completa dei fondamenti di Python.
Esempio: Funzioni utili per le stringhe
Passo 1: Scrivi il test prima (Rosso)
Crea test_string_utils.py:
import pytest
from string_utils import reverse_string, is_palindrome, count_vowels
def test_reverse_string():
assert reverse_string("hello") == "olleh"
assert reverse_string("") == ""
assert reverse_string("a") == "a"
def test_is_palindrome():
assert is_palindrome("racecar") == True
assert is_palindrome("hello") == False
assert is_palindrome("") == True
assert is_palindrome("A man a plan a canal Panama") == True
def test_count_vowels():
assert count_vowels("hello") == 2
assert count_vowels("HELLO") == 2
assert count_vowels("xyz") == 0
assert count_vowels("") == 0
Passo 2: Scrivi il codice minimo per passare (Verde)
Crea string_utils.py:
def reverse_string(s: str) -> str:
"""Inverti una stringa"""
return s[::-1]
def is_palindrome(s: str) -> bool:
"""Verifica se una stringa è un palindromo (ignorando maiuscole e spazi)"""
cleaned = ''.join(s.lower().split())
return cleaned == cleaned[::-1]
def count_vowels(s: str) -> int:
"""Conta le vocali in una stringa"""
return sum(1 for char in s.lower() if char in 'aeiou')
Passo 3: Esegui i test
pytest test_string_utils.py -v
Output:
test_string_utils.py::test_reverse_string PASSED
test_string_utils.py::test_is_palindrome PASSED
test_string_utils.py::test_count_vowels PASSED
Tecniche avanzate di test
Utilizzo di fixture per la configurazione dei test
Le fixture forniscono una configurazione e un ripulimento dei test riutilizzabili:
import pytest
from database import Database
@pytest.fixture
def db():
"""Crea un database di test"""
database = Database(":memory:")
database.create_tables()
yield database # Fornisci al test
database.close() # Pulizia dopo il test
@pytest.fixture
def sample_users(db):
"""Aggiungi utenti di esempio al database"""
db.add_user("Alice", "alice@example.com")
db.add_user("Bob", "bob@example.com")
return db
def test_get_user(sample_users):
user = sample_users.get_user_by_email("alice@example.com")
assert user.name == "Alice"
def test_user_count(sample_users):
assert sample_users.count_users() == 2
Scopi delle fixture
Controlla la durata delle fixture con gli scopi:
@pytest.fixture(scope="function") # Default: eseguito per ogni test
def func_fixture():
return create_resource()
@pytest.fixture(scope="class") # Una volta per classe di test
def class_fixture():
return create_resource()
@pytest.fixture(scope="module") # Una volta per modulo
def module_fixture():
return create_resource()
@pytest.fixture(scope="session") # Una volta per sessione di test
def session_fixture():
return create_expensive_resource()
Mocking delle dipendenze esterne
Utilizza il mocking per isolare il codice dalle dipendenze esterne:
from unittest.mock import Mock, patch, MagicMock
import requests
class WeatherService:
def get_temperature(self, city):
response = requests.get(f"https://api.weather.com/{city}")
return response.json()["temp"]
# Test con mock
def test_get_temperature():
service = WeatherService()
# Mock della funzione requests.get
with patch('requests.get') as mock_get:
# Configura la risposta del mock
mock_response = Mock()
mock_response.json.return_value = {"temp": 72}
mock_get.return_value = mock_response
# Test
temp = service.get_temperature("Boston")
assert temp == 72
# Verifica la chiamata
mock_get.assert_called_once_with("https://api.weather.com/Boston")
Utilizzo del plugin pytest-mock
pytest-mock fornisce una sintassi più pulita:
def test_get_temperature(mocker):
service = WeatherService()
# Mock con pytest-mock
mock_response = mocker.Mock()
mock_response.json.return_value = {"temp": 72}
mocker.patch('requests.get', return_value=mock_response)
temp = service.get_temperature("Boston")
assert temp == 72
Test parametrizzati
Testa efficacemente diversi scenari:
import pytest
@pytest.mark.parametrize("input,expected", [
("", True),
("a", True),
("ab", False),
("aba", True),
("racecar", True),
("hello", False),
])
def test_is_palindrome_parametrized(input, expected):
assert is_palindrome(input) == expected
@pytest.mark.parametrize("number,is_even", [
(0, True),
(1, False),
(2, True),
(-1, False),
(-2, True),
])
def test_is_even(number, is_even):
assert (number % 2 == 0) == is_even
Test delle eccezioni e gestione degli errori
import pytest
def divide(a, b):
if b == 0:
raise ValueError("Non si può dividere per zero")
return a / b
def test_divide_by_zero():
with pytest.raises(ValueError, match="Non si può dividere per zero"):
divide(10, 0)
def test_divide_by_zero_with_message():
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "zero" in str(exc_info.value).lower()
# Test che non solleva alcuna eccezione
def test_divide_success():
result = divide(10, 2)
assert result == 5.0
Test del codice asincrono
Il test del codice asincrono è essenziale per le applicazioni Python moderne, soprattutto quando si lavora con API, database o servizi AI. Ecco come testare le funzioni asincrone:
import pytest
import asyncio
async def fetch_data(url):
"""Funzione asincrona per recuperare dati"""
await asyncio.sleep(0.1) # Simula una chiamata API
return {"status": "success"}
@pytest.mark.asyncio
async def test_fetch_data():
result = await fetch_data("https://api.example.com")
assert result["status"] == "success"
@pytest.mark.asyncio
async def test_fetch_data_with_mock(mocker):
# Mock della funzione asincrona
mock_fetch = mocker.AsyncMock(return_value={"status": "mocked"})
mocker.patch('module.fetch_data', mock_fetch)
result = await fetch_data("https://api.example.com")
assert result["status"] == "mocked"
Per esempi pratici di test del codice asincrono con servizi AI, vedi la nostra guida su integrare Ollama con Python, che include strategie di test per le interazioni con i modelli LLM.
Copertura del codice
Misura quanto del tuo codice è testato:
Utilizzo di pytest-cov
# Esegui i test con la copertura
pytest --cov=myproject tests/
# Genera un rapporto HTML
pytest --cov=myproject --cov-report=html tests/
# Mostra le righe mancanti
pytest --cov=myproject --cov-report=term-missing tests/
Configurazione della copertura
Crea .coveragerc:
[run]
source = myproject
omit =
*/tests/*
*/venv/*
*/__pycache__/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
@abstractmethod
Buone pratiche per la copertura
- Mirare a una copertura del 80%+ per i percorsi critici del codice
- Non fissarsi sul 100% - concentrati sui test significativi
- Testa i casi limite non solo i percorsi felici
- Escludi il boilerplate dai rapporti di copertura
- Utilizza la copertura come guida non come obiettivo
Organizzazione dei test e struttura del progetto
Struttura consigliata
myproject/
├── myproject/
│ ├── __init__.py
│ ├── module1.py
│ ├── module2.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Fixtures condivisi
│ ├── test_module1.py
│ ├── test_module2.py
│ ├── test_utils.py
│ └── integration/
│ ├── __init__.py
│ └── test_integration.py
├── pytest.ini
├── requirements.txt
└── requirements-dev.txt
conftest.py per i fixtures condivisi
# tests/conftest.py
import pytest
from myproject.database import Database
@pytest.fixture(scope="session")
def test_db():
"""Database di test a livello di sessione"""
db = Database(":memory:")
db.create_schema()
yield db
db.close()
@pytest.fixture
def clean_db(test_db):
"""Database pulito per ogni test"""
test_db.clear_all_tables()
return test_db
Configurazione di pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--cov=myproject
--cov-report=term-missing
--cov-report=html
markers =
slow: segna i test come lenti
integration: segna i test come test di integrazione
unit: segna i test come test unitari
Migliori pratiche per i test unitari
1. Segui il modello AAA
Arrange-Act-Assert rende i test chiari:
def test_user_creation():
# Arrange
username = "john_doe"
email = "john@example.com"
# Act
user = User(username, email)
# Assert
assert user.username == username
assert user.email == email
assert user.is_active == True
2. Un’asserzione per test (guida, non regola)
# Buono: Test focalizzato
def test_user_username():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
def test_user_email():
user = User("john_doe", "john@example.com")
assert user.email == "john@example.com"
# Accettabile: Asserzioni correlate
def test_user_creation():
user = User("john_doe", "john@example.com")
assert user.username == "john_doe"
assert user.email == "john@example.com"
assert isinstance(user.created_at, datetime)
3. Utilizza nomi descrittivi per i test
# Cattivo
def test_user():
pass
# Buono
def test_user_creation_with_valid_data():
pass
def test_user_creation_fails_with_invalid_email():
pass
def test_user_password_is_hashed_after_setting():
pass
4. Testa i casi limite e i bordi
def test_age_validation():
# Casi validi
assert validate_age(0) == True
assert validate_age(18) == True
assert validate_age(120) == True
# Casi limite
assert validate_age(-1) == False
assert validate_age(121) == False
# Casi limite
with pytest.raises(TypeError):
validate_age("18")
with pytest.raises(TypeError):
validate_age(None)
5. Mantieni i test indipendenti
# Cattivo: I test dipendono dall'ordine
counter = 0
def test_increment():
global counter
counter += 1
assert counter == 1
def test_increment_again(): # Fallisce se eseguito da solo
global counter
counter += 1
assert counter == 2
# Buono: I test sono indipendenti
def test_increment():
counter = Counter()
counter.increment()
assert counter.value == 1
def test_increment_multiple_times():
counter = Counter()
counter.increment()
counter.increment()
assert counter.value == 2
6. Non testare i dettagli dell’implementazione
# Cattivo: Testa l'implementazione
def test_sort_uses_quicksort():
sorter = Sorter()
assert sorter.algorithm == "quicksort"
# Buono: Testa il comportamento
def test_sort_returns_sorted_list():
sorter = Sorter()
result = sorter.sort([3, 1, 2])
assert result == [1, 2, 3]
7. Testa i casi d’uso reali
Quando si testano librerie che elaborano o trasformano dati, concentrati sui casi d’uso reali. Ad esempio, se stai lavorando con lo scraping web o la conversione del contenuto, consulta la nostra guida su convertire HTML in Markdown con Python, che include strategie di test e confronti tra le librerie di conversione.
Integrazione con Continuous Integration
Esempio di GitHub Actions
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.9, 3.10, 3.11, 3.12]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Run tests
run: |
pytest --cov=myproject --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
Test di funzioni serverless
Quando si testano funzioni AWS Lambda o applicazioni serverless, considera strategie di test di integrazione insieme ai test unitari. La nostra guida su costruire una funzione AWS Lambda a doppio modo con Python e Terraform copre le strategie di test per le applicazioni Python serverless, incluso come testare i gestori Lambda, i consumer SQS e le integrazioni API Gateway.
Checklist delle migliori pratiche per i test
- Scrivi i test prima o insieme al codice (TDD)
- Mantieni i test veloci (< 1 secondo per test)
- Rendi i test indipendenti e isolati
- Utilizza nomi descrittivi per i test
- Segui il modello AAA (Arrange-Act-Assert)
- Testa i casi limite e le condizioni di errore
- Mocka le dipendenze esterne
- Mirare a una copertura del codice del 80%+
- Esegui i test nel pipeline CI/CD
- Rivedi e ristruttura regolarmente i test
- Documenta gli scenari di test complessi
- Utilizza le fixture per le configurazioni comuni
- Parametrizza i test simili
- Mantieni i test semplici e leggibili
Pattern comuni di test
Testare le classi
class TestUser:
@pytest.fixture
def user(self):
return User("john_doe", "john@example.com")
def test_username(self, user):
assert user.username == "john_doe"
def test_email(self, user):
assert user.email == "john@example.com"
def test_full_name(self, user):
user.first_name = "John"
user.last_name = "Doe"
assert user.full_name() == "John Doe"
Testare con file temporanei
import pytest
from pathlib import Path
@pytest.fixture
def temp_file(tmp_path):
"""Crea un file temporaneo"""
file_path = tmp_path / "test_file.txt"
file_path.write_text("test content")
return file_path
def test_read_file(temp_file):
content = temp_file.read_text()
assert content == "test content"
Testare la generazione di file
Quando si testa il codice che genera file (come PDF, immagini o documenti), utilizza directory temporanee e verifica le proprietà dei file:
@pytest.fixture
def temp_output_dir(tmp_path):
"""Fornisce una directory temporanea di output"""
output_dir = tmp_path / "output"
output_dir.mkdir()
return output_dir
def test_pdf_generation(temp_output_dir):
pdf_path = temp_output_dir / "output.pdf"
generate_pdf(pdf_path, content="Test")
assert pdf_path.exists()
assert pdf_path.stat().st_size > 0
Per esempi completi di test per la generazione di PDF, vedi la nostra guida su generare PDF in Python, che copre le strategie di test per varie librerie di generazione di PDF.
Testare con Monkeypatch
def test_environment_variable(monkeypatch):
monkeypatch.setenv("API_KEY", "test_key_123")
assert os.getenv("API_KEY") == "test_key_123"
def test_module_attribute(monkeypatch):
monkeypatch.setattr("module.CONSTANT", 42)
assert module.CONSTANT == 42
Link utili e risorse
- Documentazione di pytest
- Documentazione di unittest
- Plugin pytest-cov
- Plugin pytest-mock
- Documentazione di unittest.mock
- Test Driven Development by Example - Kent Beck
- Python Testing with pytest - Brian Okken
Risorse correlate
Fondamenti di Python e migliori pratiche
- Python Cheatsheet - Riferimento completo per la sintassi Python, le strutture dati e i pattern comuni
Test di casi specifici di Python
- Integrare Ollama con Python - Strategie di test per le integrazioni LLM e le chiamate asincrone ai servizi AI
- Convertire HTML in Markdown con Python - Approcci di test per lo scraping web e le librerie di conversione del contenuto
- Generare PDF in Python - Strategie di test per la generazione di PDF e l’output di file
Test per serverless e cloud
- Costruire una funzione AWS Lambda a doppio modo con Python e Terraform - Strategie di test per le funzioni Lambda, i consumer SQS e le integrazioni API Gateway
L’unità di test è una competenza essenziale per gli sviluppatori Python. Che tu scelga unittest o pytest, l’importante è scrivere i test in modo coerente, mantenerli manutenibili e integrarli nel tuo flusso di lavoro di sviluppo. Inizia con test semplici, adotta gradualmente tecniche avanzate come il mocking e le fixture, e utilizza gli strumenti di copertura per identificare il codice non testato.