Kodokon kodokon.com

Tests avec pytest : fixtures, paramétrage, bonnes pratiques

Structurez des tests robustes avec pytest : injection de fixtures, démontage par yield, paramétrage en table de cas et pièges des scopes larges.

9 min · 3 questions

Ouvrir cette leçon dans Kodokon

pytest repose sur deux idées fortes : la découverte par convention (fichiers test_*.py, fonctions test_*) et le simple assert, dont l'introspection affiche les valeurs réelles des deux membres en cas d'échec - plus besoin de mémoriser assertEqual et ses trente variantes. Deux outils à connaître d'emblée : pytest.raises pour vérifier qu'une exception part bien, et pytest.approx pour comparer des flottants sans vous battre avec les arrondis binaires.

PYTHON
import pytest

def divide(a: float, b: float) -> float:
    return a / b

def test_nominal() -> None:
    assert divide(8, 4) == 2.0

def test_zero_division() -> None:
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)

def test_floats() -> None:
    assert 0.1 + 0.2 == pytest.approx(0.3)
Sans approx, 0.1 + 0.2 == 0.3 est faux en flottants binaires.

Les fixtures sont l'injection de dépendances de pytest : déclarez un paramètre portant le nom d'une fixture, et pytest la construit puis l'injecte. Le yield sépare préparation et démontage - le code situé après s'exécute même si le test échoue. Le paramètre scope (function par défaut, module, session) contrôle la durée de vie de l'objet. Placez les fixtures partagées dans un conftest.py : elles deviennent disponibles dans tout le dossier, sans le moindre import.

PYTHON
import sqlite3

import pytest

@pytest.fixture
def db():
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    conn.close()

def test_insert(db) -> None:
    db.execute("INSERT INTO users VALUES ('Ada')")
    query = "SELECT name FROM users"
    rows = db.execute(query).fetchall()
    assert rows == [("Ada",)]
conn.close() tourne après le test, succès ou échec.

@pytest.mark.parametrize transforme un test en table de cas : chaque tuple devient un test indépendant, nommé et filtrable individuellement. Empilez plusieurs décorateurs parametrize pour obtenir le produit cartésien des paramètres. Donnez des id lisibles à vos cas via pytest.param(..., id='...') - un rapport d'échec doit se comprendre sans ouvrir le fichier source.

PYTHON
import pytest

def slugify(text: str) -> str:
    return "-".join(text.lower().split())

@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("Hello World", "hello-world"),
        ("  Python  rocks ", "python-rocks"),
        pytest.param("", "", id="empty"),
    ],
)
def test_slugify(raw: str, expected: str) -> None:
    assert slugify(raw) == expected
Trois cas, trois tests distincts dans le rapport.
BASH
pytest -x --lf
pytest -k "slugify and not empty"
pytest -q --durations=5
--lf ne relance que les échecs ; --durations révèle les tests lents.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Comment pytest sait-il quelle fixture fournir à un test ?
    • Grâce à un décorateur @inject posé sur le test
    • Par l'ordre de déclaration des fixtures dans le fichier
    • Par l'annotation de type du paramètre
    • Par le nom du paramètre : s'il correspond à une fixture visible (module ou conftest.py), pytest la construit et l'injecte
  2. Dans une fixture, à quoi sert le code placé après le yield ?
    • C'est le démontage : il s'exécute après le test, même si celui-ci a échoué
    • Il ne s'exécute jamais, le yield termine la fixture
    • Il fournit une valeur de repli si le setup échoue
    • Il ne s'exécute qu'en cas d'échec du test
  3. Que fait la commande pytest --lf ?
    • Elle liste les fixtures disponibles dans le projet
    • Elle ne relance que les tests ayant échoué lors de la dernière exécution
    • Elle arrête la session au premier échec rencontré