
pytest est le framework de test le plus utilisé en Python. Il permet d'écrire un test avec une simple fonction et le mot-clé assert, sans classe ni cérémonie. Ce guide part de zéro : installer pytest, écrire un premier test, le lancer, puis monter en puissance avec le paramétrage, les fixtures, le test des exceptions et la couverture de code. Tous les exemples ont été exécutés avec pytest 9.1.
Il s'adresse aux développeurs qui veulent des scripts fiables et à celles et ceux qui intègrent des tests dans un pipeline CI/CD. À la différence de unittest, intégré à Python mais plus verbeux, pytest privilégie la concision : moins de code pour le même résultat, ce qui explique son adoption massive.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Installer pytest et écrire un premier test avec
assert. - Lancer vos tests et lire un rapport d'échec.
- Paramétrer un test pour couvrir plusieurs cas sans le dupliquer.
- Factoriser la préparation avec des fixtures.
- Vérifier qu'une fonction lève bien une exception.
- Mesurer la couverture de votre code testé.
Pourquoi pytest plutôt qu'un autre outil
Section intitulée « Pourquoi pytest plutôt qu'un autre outil »Python fournit déjà unittest dans sa bibliothèque standard. Alors pourquoi installer autre chose ? Parce que pytest réduit drastiquement le code à écrire. Là où unittest impose une classe héritant de TestCase et des méthodes comme assertEqual, pytest se contente d'une fonction et du mot-clé natif assert. Le même test tient en deux fois moins de lignes et se lit d'un coup d'œil.
pytest apporte aussi un paramétrage élégant, un système de fixtures puissant pour préparer les données, des messages d'échec très détaillés (il montre les valeurs comparées), et un immense écosystème de plugins (couverture, tests asynchrones, Django). C'est aujourd'hui le standard de fait dans la communauté Python.
Installer pytest
Section intitulée « Installer pytest »pytest s'installe dans un environnement virtuel, pour ne pas polluer votre Python système.
pip install pytestuv add --dev pytestVérifiez l'installation. La sortie doit afficher la version installée :
pytest --version# pytest 9.1.1Écrire et lancer un premier test
Section intitulée « Écrire et lancer un premier test »Prenons un module à tester, calculs.py :
def addition(a, b): return a + bLe test vit dans un fichier dont le nom commence par test_, avec une fonction dont le nom commence aussi par test_. On y utilise le mot-clé assert :
from calculs import addition
def test_addition(): assert addition(2, 3) == 5Lancez la commande pytest à la racine du projet. Il découvre automatiquement les fichiers et fonctions test_* :
pytest========================= test session starts =========================collected 1 item
test_calculs.py . [100%]
========================== 1 passed in 0.01s ==========================Le point vert signale le test réussi. Cette découverte automatique est un des grands conforts de pytest : pas de liste de tests à maintenir.
Lire un échec
Section intitulée « Lire un échec »L'intérêt d'un test est de signaler une régression. Si addition renvoyait un résultat faux, pytest afficherait précisément l'écart :
def test_addition():> assert addition(2, 3) == 5E assert 6 == 5E + where 6 = addition(2, 3)
test_calculs.py:5: AssertionError========================== 1 failed in 0.02s ==========================pytest montre la ligne fautive, les valeurs comparées (6 == 5) et d'où vient la valeur. Pas besoin d'ajouter des print() : le rapport contient déjà l'essentiel pour comprendre.
Quelques options utiles au quotidien :
pytest -v # détaille chaque test (verbose)pytest -q # sortie compactepytest test_calculs.py # un seul fichierpytest -k addition # seulement les tests dont le nom contient "addition"pytest -x # s'arrête au premier échecCouvrir plusieurs cas avec parametrize
Section intitulée « Couvrir plusieurs cas avec parametrize »Tester addition sur une seule paire de valeurs est insuffisant. Plutôt que d'écrire un test par cas, le décorateur @pytest.mark.parametrize rejoue la même fonction avec plusieurs jeux de données :
import pytest
from calculs import addition
@pytest.mark.parametrize("a, b, attendu", [ (2, 3, 5), (-1, 1, 0), (0, 0, 0),])def test_addition(a, b, attendu): assert addition(a, b) == attendupytest compte alors trois tests distincts, un par ligne, et nomme chacun par ses valeurs :
test_calculs.py::test_addition[2-3-5] PASSEDtest_calculs.py::test_addition[-1-1-0] PASSEDtest_calculs.py::test_addition[0-0-0] PASSEDSi un cas échoue, vous savez immédiatement lequel. C'est la façon idiomatique de couvrir les cas limites sans dupliquer le code.
Factoriser avec les fixtures
Section intitulée « Factoriser avec les fixtures »Une fixture prépare un contexte réutilisable (des données, une connexion, un fichier temporaire) et l'injecte dans les tests qui en ont besoin. On la déclare avec @pytest.fixture, et un test la reçoit simplement en la nommant dans ses paramètres :
import pytest
@pytest.fixturedef utilisateur(): return {"nom": "Alice", "roles": ["admin"]}
def test_droits_admin(utilisateur): assert "admin" in utilisateur["roles"]
def test_nom(utilisateur): assert utilisateur["nom"] == "Alice"Chaque test reçoit une instance fraîche de la fixture, ce qui garantit leur indépendance : un test ne peut pas polluer l'autre. pytest fournit aussi des fixtures intégrées très utiles, comme tmp_path pour un dossier temporaire propre à chaque test.
Vérifier qu'une exception est levée
Section intitulée « Vérifier qu'une exception est levée »Tester le chemin d'erreur est aussi important que le cas nominal. Le gestionnaire de contexte pytest.raises vérifie qu'un bloc lève bien l'exception attendue, et match contrôle le message :
import pytest
from calculs import diviser
def test_division_par_zero(): with pytest.raises(ValueError, match="division par zéro"): diviser(10, 0)Pour comparer des nombres flottants, dont l'égalité stricte est piégeuse, utilisez pytest.approx :
def test_resultat_flottant(): assert diviser(10, 3) == pytest.approx(3.3333, rel=1e-3)Mesurer la couverture de code
Section intitulée « Mesurer la couverture de code »La couverture indique quelle part de votre code est réellement exécutée par les tests. Le plugin pytest-cov l'ajoute à pytest :
pip install pytest-covpytest --cov=calculs --cov-report=term-missingName Stmts Miss Cover Missing------------------------------------------calculs.py 6 0 100%------------------------------------------TOTAL 6 0 100%La colonne Missing liste les lignes jamais exécutées par un test, donc non vérifiées.
Organiser ses tests
Section intitulée « Organiser ses tests »Sur un vrai projet, on sépare le code des tests dans un dossier tests/ dédié. Les fixtures communes vont dans un fichier conftest.py, que pytest charge automatiquement pour tout le dossier.
Répertoiremon_projet/
- calculs.py
Répertoiretests/
- conftest.py
- test_calculs.py
Ce découpage garde le projet lisible et permet de lancer toute la suite d'une seule commande pytest. Intégrée dans un pipeline CI/CD, cette commande devient un filet de sécurité : chaque modification est validée automatiquement avant d'être fusionnée ou déployée.
À retenir
Section intitulée « À retenir »- pytest teste avec une simple fonction
test_*et le mot-cléassert, sans classe. - La commande
pytestdécouvre seule les fichiers et fonctions de test. - En cas d'échec, le rapport montre les valeurs comparées et la ligne fautive.
@pytest.mark.parametrizerejoue un test sur plusieurs cas sans le dupliquer.- Une fixture prépare un contexte réutilisable et indépendant entre tests.
pytest.raisesvérifie les exceptions,pytest.approxcompare les flottants.pytest-covmesure la couverture, mais 100 % ne garantit pas l'absence de bugs.- pytest exécute aussi les tests unittest : la migration peut être progressive.
FAQ : pytest en Python
Section intitulée « FAQ : pytest en Python »assert :# test_calculs.py
from calculs import addition
def test_addition():
assert addition(2, 3) == 5
Règles de découverte : le fichier et la fonction commencent par test_. Lancez ensuite :pytest
pytest découvre et exécute automatiquement tous les tests, sans classe ni configuration. C'est ce qui le rend plus concis que unittest.| unittest | pytest | |
|---|---|---|
| Disponibilité | intégré à Python | à installer |
| Structure | classes TestCase |
fonctions simples |
| Vérification | assertEqual, assertTrue |
assert natif |
| Messages d'échec | basiques | détaillés (valeurs comparées) |
pytest test_calculs.py # un fichier
pytest test_calculs.py::test_addition # un test précis
pytest -k addition # les tests dont le nom contient addition
pytest -v # détaille chaque test
pytest -x # stoppe au premier échec
pytest --lf # relance seulement les derniers échoués
Ces options se combinent et font gagner beaucoup de temps pendant le développement, en ne relançant que ce qui vous intéresse.import pytest
@pytest.fixture
def utilisateur():
return {"nom": "Alice", "roles": ["admin"]}
def test_droits(utilisateur):
assert "admin" in utilisateur["roles"]
Le test reçoit la fixture en la nommant en paramètre. Chaque test obtient une instance fraîche, garantissant leur indépendance. Les fixtures partagées se placent dans conftest.py, chargé automatiquement.pytest.raises vérifie qu'un bloc lève l'exception attendue :import pytest
def test_division_par_zero():
with pytest.raises(ValueError, match="division par zéro"):
diviser(10, 0)
L'argument match contrôle le message (expression régulière). Le test réussit si l'exception est levée, échoue sinon. C'est indispensable pour tester le chemin d'erreur, pas seulement le cas nominal.pytest-cov ajoute la couverture à pytest :pip install pytest-cov
pytest --cov=calculs --cov-report=term-missing
Name Stmts Miss Cover Missing
calculs.py 6 0 100%
La colonne Missing liste les lignes jamais exécutées. Attention : 100 % prouve que les lignes sont exécutées, pas que les assertions vérifient réellement le comportement. La couverture mesure une quantité, pas une qualité.Prochaines étapes
Section intitulée « Prochaines étapes »Une fois pytest en main, on compare avec la bibliothèque standard, on lance la suite sur plusieurs versions de Python et on l'intègre au pipeline.