Tu viens encore de lancer ton script Ă  la main, de lire la sortie console ligne par ligne et de te convaincre que « oui, ça a l’air bon » ? Chaque exĂ©cution manuelle te coĂ»te une Ă  cinq minutes. Multiplie par dix vĂ©rifications par jour, par cinq jours par semaine : c’est une demi-journĂ©e de travail envolĂ©e, chaque semaine, pour une tĂąche qu’un ordinateur ferait mieux que toi en quelques millisecondes.

C’est exactement le problĂšme que rĂ©sout pytest, le framework de tests le plus utilisĂ© de l’Ă©cosystĂšme Python. Dans ce tutoriel, tu vas apprendre Ă  Ă©crire des tests automatisĂ©s, lisibles et maintenables : assertions expressives, fixtures pour prĂ©parer tes donnĂ©es, parametrize pour multiplier les cas sans dupliquer le code, markers pour organiser tes campagnes de tests. À la fin, ta suite de tests tournera en une seule commande.

Pourquoi arrĂȘter les tests manuels

Le test manuel souffre de quatre dĂ©fauts qui s’accumulent avec le temps :

  • C’est lent. Un test automatisĂ© s’exĂ©cute en secondes ; sa version manuelle prend des minutes, et tu dois ĂȘtre devant l’Ă©cran.
  • C’est peu fiable. Ton attention diminue au fil de la journĂ©e. Tu finis par survoler les rĂ©sultats, surtout quand rien n’a changĂ© depuis hier.
  • C’est non reproductible. Impossible de garantir que ton collĂšgue teste exactement les mĂȘmes scĂ©narios que toi, dans le mĂȘme ordre.
  • C’est un frein aux modifications. Sans filet de sĂ©curitĂ©, chaque changement de code devient stressant : tu ne sais jamais si tu viens de casser quelque chose ailleurs.

Une suite de tests pytest inverse cette logique : elle rejoue l’intĂ©gralitĂ© de tes scĂ©narios en une commande, dĂ©tecte immĂ©diatement les rĂ©gressions, et sert de documentation vivante du comportement attendu de ton code. C’est aussi un excellent rĂ©flexe professionnel : la majoritĂ© des Ă©quipes de dĂ©veloppement exigent des tests avant toute mise en production.

Installation de pytest

PremiĂšre Ă©tape : installer pytest dans l’environnement virtuel de ton projet. Si tu gĂšres tes dĂ©pendances avec Poetry, jette un Ɠil Ă  mon guide complet sur Poetry.

# Depuis la racine de ton projet, avec le venv activé
pip install pytest

# Vérifie l'installation
pytest --version

Ajoute ensuite pytest Ă  tes dĂ©pendances de dĂ©veloppement pour que tes collĂšgues (et ton futur toi) disposent du mĂȘme outillage :

pip install --group dev pytest        # pip 25.1+
# ou classique :
pip freeze | grep pytest >> requirements-dev.txt

C’est tout : aucune configuration n’est obligatoire pour dĂ©marrer. pytest dĂ©couvre et lance automatiquement tous les fichiers commençant par test_ ou finissant par _test.py.

Ton premier test avec pytest

Imaginons un petit module calcul.py avec une fonction Ă  tester :

# calcul.py
def additionner(a, b):
    """Retourne la somme de a et b."""
    return a + b

Crée un fichier test_calcul.py à cÎté (ou mieux, dans un dossier tests/, on y reviendra). La convention est simple : une fonction de test commence par test_ et contient au moins une assertion.

# test_calcul.py
from calcul import additionner

def test_additionner_deux_nombres():
    resultat = additionner(2, 3)
    assert resultat == 5

Lance la suite :

$ python -m pytest -v
================= test session starts ==================
collected 1 item

test_calcul.py::test_additionner_deux_nombres PASSED [100%]

================== 1 passed in 0.01s ===================

Note deux choses. D’abord, pas besoin de classe ni d’hĂ©ritage : une fonction suffit, contrairement au module standard unittest. Ensuite, utilise toujours python -m pytest plutĂŽt que pytest seul : cela ajoute le dossier courant au chemin Python et Ă©vite les erreurs d’import selon la structure de ton projet.

L’assertion : le cƓur de pytest

Dans pytest, on n’utilise qu’une seule instruction : le bon vieux assert natif de Python. Pas besoin d’apprendre une vingtaine de mĂ©thodes comme assertEqual ou assertTrue. LĂ  oĂč pytest devient redoutable, c’est dans son rapport d’Ă©chec : quand une assertion Ă©choue, il t’affiche la valeur rĂ©elle de chaque variable impliquĂ©e.

def test_additionner_negatifs():
    resultat = additionner(-2, -3)
    assert resultat == -4   # volontairement faux
E       assert -5 == -4
E        +  where -5 = additionner(-2, -3)

Tu vois immĂ©diatement quoi corriger, sans relancer le programme en mode debug. Quelques formes d’assertions utiles au quotidien :

import pytest

def verifier_email(email):
    if "@" not in email:
        raise ValueError("email invalide")
    return email.strip().lower()

def test_normalisation():
    assert verifier_email("  Alice@Exemple.COM ") == "alice@exemple.com"

def test_email_sans_arobase():
    with pytest.raises(ValueError):
        verifier_email("pas-un-email")

def test_collection():
    paniers = ["A", "B", "C"]
    assert "B" in paniers
    assert len(paniers) == 3

La forme with pytest.raises(...) remplace le pĂ©nible try/except manuel : elle Ă©choue si l’exception n’est pas levĂ©e, et tu peux mĂȘme vĂ©rifier son message avec match :

def test_message_erreur():
    with pytest.raises(ValueError, match="email invalide"):
        verifier_email("oops")

Les fixtures : préparer proprement tes données de test

Un bon test suit souvent la structure Arrange – Act – Assert : prĂ©parer les donnĂ©es, dĂ©clencher l’action, vĂ©rifier le rĂ©sultat. Quand la phase « Arrange » devient longue (crĂ©er un objet complexe, remplir une base temporaire, Ă©crire un fichier), les fixtures entrent en jeu. Une fixture est une fonction annotĂ©e @pytest.fixture que tes tests dĂ©clarent simplement en paramĂštre : pytest la construit et l’injecte automatiquement.

# test_panier.py
import pytest
from panier import Panier

@pytest.fixture
def panier_rempli():
    """Un panier prĂȘt Ă  l'emploi avec trois articles."""
    p = Panier()
    p.ajouter("clé USB", prix=9.90, quantite=2)
    p.ajouter("cĂąble HDMI", prix=12.50, quantite=1)
    return p

def test_total_panier(panier_rempli):
    assert panier_rempli.total() == pytest.approx(32.30)

def test_nombre_articles(panier_rempli):
    assert panier_rempli.nombre_articles() == 3

def test_panier_vide():
    assert Panier().total() == 0

Chaque test qui demande panier_rempli reçoit une instance neuve : aucun test ne pollue les donnĂ©es d’un autre, donc l’ordre d’exĂ©cution n’a aucune importance. Deux points de vigilance :

  • Nombres flottants : compare toujours avec pytest.approx(), sinon 0.1 + 0.2 == 0.3 Ă©chouera Ă  cause de l’arrondi binaire.
  • Ressources Ă  libĂ©rer : ajoute du code aprĂšs le yield pour nettoyer (fichier temporaire, connexion
). pytest garantit l’exĂ©cution mĂȘme si le test Ă©choue.
@pytest.fixture
def fichier_temporaire(tmp_path):
    chemin = tmp_path / "donnees.csv"
    chemin.write_text("produit;prix\ncle_usb;9.90\n")
    yield chemin          # le test s'exécute ici
    # nettoyage automatique : tmp_path est déjà géré par pytest

Au passage, tmp_path est une fixture intĂ©grĂ©e qui fournit un dossier temporaire unique par test — inutile de coder ta propre gestion de fichiers temporaires. Autre force : les fixtures composables. Une fixture peut dĂ©pendre d’autres fixtures, et une fixture dĂ©clarĂ©e dans un fichier conftest.py devient disponible pour tous les tests du dossier, sans import :

# tests/conftest.py — partagĂ© par toute la suite
import pytest
from panier import Panier

@pytest.fixture
def catalogue():
    return {"clé USB": 9.90, "cùble HDMI": 12.50}

@pytest.fixture
def panier_rempli(catalogue):
    p = Panier(catalogue)
    p.ajouter("clé USB", quantite=2)
    p.ajouter("cĂąble HDMI", quantite=1)
    return p

RÚgle pratique : une fixture utilisée par un seul fichier reste dans ce fichier ; dÚs que deux fichiers de tests en ont besoin, elle migre dans conftest.py.

Parametrize : tester vingt cas sans dupliquer le code

Sans @pytest.mark.parametrize, tester une fonction sur dix entrées différentes se traduit par dix fonctions quasi identiques. Avec parametrize, tu écris le test une fois et tu listes les cas en paramÚtres :

# nombres.py
def est_premier(n):
    if n < 2:
        return False
    for diviseur in range(2, int(n ** 0.5) + 1):
        if n % diviseur == 0:
            return False
    return True
# test_nombres.py
import pytest
from nombres import est_premier

@pytest.mark.parametrize("nombre, attendu", [
    (2, True),
    (3, True),
    (4, False),
    (17, True),
    (25, False),
    (97, True),
])
def test_est_premier(nombre, attendu):
    assert est_premier(nombre) == attendu

pytest gĂ©nĂšre six tests indĂ©pendants, affichĂ©s sĂ©parĂ©ment dans le rapport. En cas d’Ă©chec, tu vois exactement quel couple (nombre, attendu) pose problĂšme. Tu peux aussi donner un identifiant lisible Ă  chaque cas, prĂ©cieux pour les jeux de donnĂ©es complexes :

@pytest.mark.parametrize("nombre, attendu", [
    pytest.param(2, True, id="plus-petit-premier"),
    pytest.param(4, False, id="carre-parfait"),
    pytest.param(-7, False, id="negatif"),
])
def test_est_premier_lisible(nombre, attendu):
    assert est_premier(nombre) == attendu

Bonne habitude : inclue toujours les cas limites — zĂ©ro, valeurs nĂ©gatives, liste vide, chaĂźne vide, maximum autorisĂ©. Ce sont eux qui cachent la plupart des bugs.

Les markers : organiser et filtrer tes tests

Tous les tests n’ont pas la mĂȘme durĂ©e ni la mĂȘme criticitĂ©. Les markers permettent d’Ă©tiqueter puis de filtrer. Les marqueurs intĂ©grĂ©s les plus utiles :

import sys
import pytest

@pytest.mark.slow
def test_generation_rapport_complet():
    ...  # traitement de plusieurs minutes

@pytest.mark.skipif(sys.platform == "win32",
                    reason="dépend d'outils Linux")
def test_script_shell():
    ...

@pytest.mark.xfail(reason="bug #42, correction en cours")
def test_tri_cas_limite():
    ...
# Lancer uniquement les tests rapides
pytest -m "not slow"

# Lancer seulement la génération de rapport
pytest -m slow

Pour Ă©viter l’avertissement PytestUnknownMarkWarning, dĂ©clare tes marqueurs personnalisĂ©s dans un fichier pytest.ini Ă  la racine :

[pytest]
markers =
    slow: tests longs, Ă  lancer avant les releases
addopts = -ra

Retiens aussi ces options en ligne de commande qui font gagner du temps : -k nom pour filtrer par mot-clé dans le nom du test, --lf pour ne relancer que les tests échoués précédemment, -x pour stopper au premier échec, et --tb=short pour des tracebacks compacts.

Organiser ses fichiers de tests

Sur un projet réel, la structure recommandée sépare clairement le code applicatif du code de test :

mon_projet/
├── src/
│   └── boutique/
│       ├── panier.py
│       └── facturation.py
├── tests/
│   ├── conftest.py            # fixtures partagĂ©es
│   ├── test_panier.py
│   └── test_facturation.py
├── pytest.ini                 # config : markers, options
└── requirements.txt

Trois rĂšgles simples suffisent :

  • Un fichier de tests par module testĂ© : panier.py → test_panier.py. Tout le monde retrouve son chemin.
  • Des noms explicites : test_total_panier_avec_reduction plutĂŽt que test_panier2. Le nom doit raconter le scĂ©nario.
  • Un seul comportement par test : un test qui vĂ©rifie cinq choses Ă©choue sans dire laquelle. DĂ©coupe-le.

Pour mesurer la couverture — le pourcentage de lignes exĂ©cutĂ©es par tes tests — installe le plugin officiel :

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

Vise une couverture élevée sur la logique métier, mais ne chasse pas le 100 % aveuglément : un test qui valide un comportement réel vaut mieux que trois tests cosmétiques.

Huit bonnes pratiques pour des tests propres

  1. Structure Arrange–Act–Assert. Trois blocs visibles dans chaque test : prĂ©paration, action, vĂ©rification. Un commentaire par bloc suffit quand le test grandit.
  2. Nomme tes tests comme des phrases. test_retrait_superieur_au_sol_leve_une_exception se lit sans ouvrir le fichier.
  3. Isole chaque test. Aucun test ne doit dĂ©pendre du rĂ©sultat d’un autre ni de donnĂ©es partagĂ©es modifiables. Les fixtures garantissent un Ă©tat neuf.
  4. Utilise parametrize pour les variations, jamais le copier-coller de tests presque identiques.
  5. Teste le comportement public, pas les dĂ©tails internes. Si tu dois refactorer sans casser tes tests, ils sont bien Ă©crits ; s’ils cassent Ă  chaque retouche mineure, ils collent trop Ă  l’implĂ©mentation.
  6. Garde la suite rapide. Au-delà de quelques dizaines de secondes, tu ne la lanceras plus spontanément. Marque les tests lourds @pytest.mark.slow et simule les appels réseau avec monkeypatch ou le plugin responses.
  7. Échoue volontairement une fois. Écris le test, vĂ©rifie qu’il Ă©choue sur un bug rĂ©el, puis corrige le code. Un test qui n’a jamais Ă©tĂ© rouge peut ĂȘtre faux et vert pour de mauvaises raisons.
  8. Lance pytest dans l’intĂ©gration continue. GitHub Actions ou GitLab CI rejouent la suite Ă  chaque push : les rĂ©gressions sont dĂ©tectĂ©es avant la fusion. Pour aller plus loin sur la qualitĂ© globale, lis mon article sur la revue de code efficace.

Conclusion : ta premiĂšre suite de tests, aujourd’hui

Récapitulons ton kit de départ : assert pour toutes les vérifications, pytest.raises pour les exceptions, @pytest.fixture pour la préparation des données, @pytest.mark.parametrize pour couvrir tous les cas, les markers pour trier rapide et lent, et un dossier tests/ propre avec son conftest.py. Rien de tout cela ne demande plus de dix minutes de mise en place.

Mon conseil : choisis un module existant de ton projet — idĂ©alement une petite fonction pure — et Ă©cris trois tests dessus dĂšs aujourd’hui : le cas nominal, un cas limite, un cas d’erreur. ExĂ©cute python -m pytest -v et savoure la sensation de voir six vĂ©rifications passer en une seconde. Ensuite, Ă©tends progressivement : les tests s’Ă©crivent mieux par petites touches rĂ©guliĂšres qu’en marathon.

Si tu veux consolider tes bases Python avant d’industrialiser tes tests, notre formation Python : Les Fondamentaux t’accompagne pas Ă  pas, avec des exercices corrigĂ©s et un accompagnement humain — car un bon outil comme pytest ne remplace pas un professeur : il multiplie ce que ton prof t’a appris Ă  faire. Pour approfondir le sujet, consulte aussi le guide complet sur les tests unitaires et d’intĂ©gration avec pytest.

Et toi, combien de temps passes-tu encore à tester manuellement ? Lance la conversation en commentaire, je réponds à toutes les questions techniques.

Catégories : Formations