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 --versionAjoute 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.txtC’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 + bCré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 == 5Lance 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 fauxE 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) == 3La 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() == 0Chaque 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(), sinon0.1 + 0.2 == 0.3Ă©chouera Ă cause de l’arrondi binaire. - Ressources Ă libĂ©rer : ajoute du code aprĂšs le
yieldpour 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 pytestAu 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 pRÚ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) == attendupytest 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) == attenduBonne 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 slowPour Ă©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 = -raRetiens 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.txtTrois 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_reductionplutÎt quetest_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-missingVise 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
- Structure ArrangeâActâAssert. Trois blocs visibles dans chaque test : prĂ©paration, action, vĂ©rification. Un commentaire par bloc suffit quand le test grandit.
- Nomme tes tests comme des phrases.
test_retrait_superieur_au_sol_leve_une_exceptionse lit sans ouvrir le fichier. - 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.
- Utilise parametrize pour les variations, jamais le copier-coller de tests presque identiques.
- 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.
- Garde la suite rapide. Au-delà de quelques dizaines de secondes, tu ne la lanceras plus spontanément. Marque les tests lourds
@pytest.mark.slowet simule les appels rĂ©seau avec monkeypatch ou le pluginresponses. - Ă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.
- 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.