W3docs

Unit Testing in Python con pytest

Impara pytest da zero: asserzioni, fixture, parametrizzazione dei test e organizzazione della suite con conftest.py.

pytest è il framework di testing più popolare per Python. Permette di scrivere funzioni di test piccole e leggibili usando semplici istruzioni assert — senza classi boilerplate — mantenendo al tempo stesso la scalabilità per suite di test complesse con fixture condivise, parametrizzazione e plugin.

Questo capitolo copre tutto ciò che serve per testare il codice Python con pytest: installazione, scrittura del primo test, asserzioni ed eccezioni attese, fixture, parametrize, organizzazione dei test con conftest.py, opzioni della riga di comando utili e i problemi più comuni.

Perché pytest?

Python include il modulo unittest, quindi perché usare pytest al suo posto?

Caratteristicaunittestpytest
Sintassi dei testClasse + metodoFunzione semplice
Asserzioniself.assertEqual(a, b)assert a == b
FixturesetUp / tearDown@pytest.fixture (componibili)
ParametrizeCiclo manuale@pytest.mark.parametrize
Ecosistema di pluginMinimaleOltre 1 000 plugin (coverage, mock, ecc.)

pytest esegue anche i test in stile unittest senza modifiche, quindi puoi adottarlo gradualmente.

Installazione

pytest non fa parte della libreria standard. Installalo con pip all'interno di un ambiente virtuale:

python -m venv .venv
source .venv/bin/activate     # Windows: .venv\Scripts\activate
pip install pytest

Verifica l'installazione:

pytest --version
# pytest 8.x.x

Consulta Python pip se hai bisogno di un ripasso sulla gestione dei pacchetti.

Il Primo Test

pytest scopre automaticamente i file di test. Per impostazione predefinita cerca:

  • File denominati test_*.py o *_test.py
  • Funzioni il cui nome inizia con test_

Crea math_utils.py con una semplice funzione:

# math_utils.py

def add(a, b):
    return a + b

Ora crea test_math_utils.py nella stessa directory:

# test_math_utils.py
from math_utils import add

def test_add_positive_numbers():
    assert add(2, 3) == 5

def test_add_negative_numbers():
    assert add(-1, 1) == 0

def test_add_zeros():
    assert add(0, 0) == 0

Esegui i test:

pytest test_math_utils.py

Output:

collected 3 items

test_math_utils.py ...                                                 [100%]

3 passed in 0.01s

Ogni punto rappresenta un test superato. Un test fallito stampa F e mostra il diff completo dell'asserzione.

Asserzioni

pytest riscrive le semplici istruzioni assert durante la raccolta in modo che i fallimenti mostrino un diff dettagliato — senza bisogno di metodi di asserzione speciali.

def test_assertion_diff():
    result = [1, 2, 4]
    expected = [1, 2, 3]
    assert result == expected   # pytest shows exactly where lists differ

Un output di fallimento appare così:

AssertionError: assert [1, 2, 4] == [1, 2, 3]
  At index 2: 4 != 3

Confronti con Virgola Mobile

Non confrontare mai i float con == — gli errori di arrotondamento lo rendono inaffidabile. Usa pytest.approx:

import pytest
import math

def circle_area(r):
    return math.pi * r * r

def test_circle_area():
    assert circle_area(5) == pytest.approx(78.53981633974483)

pytest.approx accetta una tolleranza opzionale abs o rel:

assert 0.1 + 0.2 == pytest.approx(0.3, abs=1e-9)

Testare le Eccezioni Attese

Usa pytest.raises come context manager per asserire che viene sollevata una specifica eccezione:

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

L'argomento match è un'espressione regolare verificata rispetto al messaggio dell'eccezione. Se l'eccezione non viene sollevata, pytest fa fallire il test — assicurandosi di intercettare le regressioni dove la gestione degli errori viene rimossa accidentalmente.

Consulta Python Try...Except per un approfondimento sulla gestione delle eccezioni, e Raising Exceptions per come sollevarle intenzionalmente.

Parametrize: Eseguire un Test con Molti Input

@pytest.mark.parametrize consente di eseguire la stessa logica di test su più set di dati senza scrivere un ciclo:

import pytest
from math_utils import add

@pytest.mark.parametrize("a, b, expected", [
    (2,  3,  5),
    (-1, 1,  0),
    (0,  0,  0),
    (10, -5, 5),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

pytest genera un caso di test separato per ogni tupla e li riporta individualmente:

test_math_utils.py::test_add[2-3-5] PASSED
test_math_utils.py::test_add[-1-1-0] PASSED
test_math_utils.py::test_add[0-0-0] PASSED
test_math_utils.py::test_add[10--5-5] PASSED

Questo approccio è molto più pulito di un ciclo manuale — i fallimenti individuali sono isolati e facili da identificare.

Fixture

Una fixture è una funzione decorata con @pytest.fixture che fornisce setup condiviso (e teardown opzionale) per i test. Invece di ripetere il codice di setup in ogni test, dichiari una fixture una volta e la inietti per nome come parametro del test.

Fixture di Base

import pytest

class UserStore:
    def __init__(self):
        self.users = []

    def add_user(self, name):
        self.users.append(name)

    def count(self):
        return len(self.users)

@pytest.fixture
def store():
    return UserStore()

def test_empty_store(store):
    assert store.count() == 0

def test_add_user(store):
    store.add_user("Alice")
    assert store.count() == 1

pytest vede che test_add_user ha un parametro chiamato store, cerca una fixture con quel nome, la chiama e passa il risultato. Ogni test riceve un'istanza di fixture nuova — le modifiche in un test non si ripercuotono mai su un altro.

Fixture con Teardown (yield)

Usa yield all'interno di una fixture per dividerla in setup (prima di yield) e teardown (dopo yield). Questo assicura che la pulizia venga sempre eseguita, anche se il test fallisce:

import pytest
import tempfile
import os

@pytest.fixture
def temp_file():
    fd, path = tempfile.mkstemp(suffix=".txt")
    os.close(fd)
    yield path           # test receives the path here
    if os.path.exists(path):
        os.unlink(path)  # always runs after the test

def test_write_to_temp_file(temp_file):
    with open(temp_file, "w") as f:
        f.write("hello")
    with open(temp_file) as f:
        assert f.read() == "hello"

Scope delle Fixture

Per impostazione predefinita, le fixture vengono create e distrutte una volta per ogni funzione di test. Puoi ampliare lo scope per ridurre i setup costosi:

ScopeCreato una volta per
"function" (predefinito)Ogni funzione di test
"class"Ogni classe di test
"module"Ogni file di test
"session"Intera esecuzione dei test
@pytest.fixture(scope="session")
def database_connection():
    conn = create_db_connection()
    yield conn
    conn.close()

Usa lo scope "session" per risorse costose come connessioni al database o processi server. Usa lo scope "function" (il predefinito) per qualsiasi cosa che modifica lo stato.

Fixture Integrate

pytest include diverse fixture integrate utilizzabili senza importare nulla:

  • tmp_path — un pathlib.Path che punta a una directory temporanea unica per il test.
  • monkeypatch — sostituisce attributi, variabili d'ambiente o voci di dizionario per la durata di un test, poi ripristina automaticamente.
  • capsys — cattura l'output di stdout / stderr in modo da poter fare asserzioni sul testo stampato.
def greet(name):
    print(f"Hello, {name}!")

def test_greet_output(capsys):
    greet("World")
    captured = capsys.readouterr()
    assert captured.out == "Hello, World!\n"

Usare monkeypatch

monkeypatch è il modo idiomatico per sostituire le dipendenze esterne nei test senza una libreria mock di terze parti:

import time

def get_timestamp():
    return time.time()

def test_get_timestamp(monkeypatch):
    monkeypatch.setattr(time, "time", lambda: 1_000_000.0)
    assert get_timestamp() == 1_000_000.0

Dopo il test, time.time viene ripristinato alla sua implementazione originale. Consulta Python Decorators se vuoi capire come funziona @pytest.fixture internamente.

Organizzare i Test con conftest.py

Quando una fixture è necessaria per test in più file, inseriscila in conftest.py. pytest scopre automaticamente i file conftest.py e rende le loro fixture disponibili a tutti i test nella stessa directory e nelle sottodirectory — senza bisogno di importare nulla.

project/
├── conftest.py          # shared fixtures live here
├── test_users.py
├── test_orders.py
└── utils/
    ├── conftest.py      # fixtures scoped to this subdirectory
    └── test_helpers.py
# conftest.py
import pytest

@pytest.fixture
def admin_user():
    return {"name": "Admin", "role": "admin", "active": True}
# test_users.py  — no import needed; pytest injects admin_user automatically
def test_admin_is_active(admin_user):
    assert admin_user["active"] is True

Test Basati su Classi

Puoi raggruppare test correlati in una classe. A differenza di unittest.TestCase, le classi pytest non richiedono ereditarietà:

class TestCalculator:
    def test_add(self):
        assert 2 + 2 == 4

    def test_multiply(self):
        assert 3 * 4 == 12

    def test_subtract(self):
        assert 10 - 3 == 7

Le classi sono utili per raggruppare test che condividono un aspetto logico. Evita le classi quando il raggruppamento non apporta un vero vantaggio — le funzioni piatte sono più semplici.

Mark: Salto e Etichette Personalizzate

Il sistema di mark di pytest consente di annotare i test con metadati per un'esecuzione selettiva.

Saltare un Test

import pytest
import sys

@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
    assert False

@pytest.mark.skipif(sys.platform == "win32", reason="Linux only")
def test_linux_feature():
    assert True

Mark Personalizzati

Registra mark personalizzati in pytest.ini (o pyproject.toml) per taggare i test per categoria:

# pytest.ini
[pytest]
markers =
    slow: marks tests as slow (deselect with -m "not slow")
    integration: marks integration tests
@pytest.mark.slow
def test_large_dataset():
    ...

Esegui solo i test lenti:

pytest -m slow

Esegui tutto tranne i test lenti:

pytest -m "not slow"

Opzioni Utili della Riga di Comando

pytest                          # run all discovered tests
pytest test_math_utils.py       # run a specific file
pytest test_math_utils.py::test_add  # run one test by name
pytest -v                       # verbose: show each test name
pytest -x                       # stop on first failure
pytest --tb=short               # shorter traceback (default is long)
pytest -k "add"                 # run tests whose name contains "add"
pytest --lf                     # re-run only last-failing tests
pytest -q                       # quiet: minimal output

Copertura dei Test

Installa il plugin di copertura per misurare quali righe vengono esercitate dai tuoi test:

pip install pytest-cov
pytest --cov=math_utils --cov-report=term-missing

L'output aggiunge una colonna di copertura che mostra quali righe non sono state raggiunte:

Name            Stmts   Miss  Cover   Missing
---------------------------------------------
math_utils.py       2      0   100%

Punta a una copertura elevata sulla logica di business critica, ma non inseguire il 100% — testare getter banali spesso aggiunge rumore senza valore.

Problemi Comuni

1. Fixture non trovata. Se pytest riporta fixture 'foo' not found, verifica che la fixture sia in conftest.py o nello stesso file, e che la funzione sia decorata con @pytest.fixture.

2. Errori di importazione durante la raccolta. Se pytest non riesce a importare il tuo modulo, segnala un errore prima di eseguire qualsiasi test. Esegui python -c "import your_module" per diagnosticare.

3. Argomenti predefiniti mutabili nelle fixture. Come le normali funzioni Python, le fixture dovrebbero evitare argomenti predefiniti mutabili. Usa lo scope "function" (il predefinito) per qualsiasi fixture che costruisce un object mutabile.

4. assert nelle funzioni helper. Se chiami un helper da un test e quell'helper contiene assert, assicurati che il suo nome inizi con assert_ (convenzione pytest) in modo che pytest riscriva l'asserzione per un messaggio di errore migliore.

5. Mescolare unittest.TestCase e fixture pytest. pytest esegue i test unittest.TestCase, ma non puoi iniettare fixture pytest nei metodi di TestCase. Usa le classi in stile pytest o i metodi di setup di unittest — non entrambi contemporaneamente.

Esercitati

Pratica
Which decorator marks a pytest function as a fixture?
Which decorator marks a pytest function as a fixture?
Pratica
What does pytest.approx() help you do in tests?
What does pytest.approx() help you do in tests?
Pratica
Where should you put fixtures that need to be shared across multiple test files?
Where should you put fixtures that need to be shared across multiple test files?
Was this page helpful?