Aller au contenu
Développement medium

pytest : écrire et lancer des tests en Python

10 min de lecture

logo python

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.

  • 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é.

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.

pytest s'installe dans un environnement virtuel, pour ne pas polluer votre Python système.

Fenêtre de terminal
pip install pytest

Vérifiez l'installation. La sortie doit afficher la version installée :

Fenêtre de terminal
pytest --version
# pytest 9.1.1

Prenons un module à tester, calculs.py :

calculs.py
def addition(a, b):
return a + b

Le 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 :

test_calculs.py
from calculs import addition
def test_addition():
assert addition(2, 3) == 5

Lancez la commande pytest à la racine du projet. Il découvre automatiquement les fichiers et fonctions test_* :

Fenêtre de terminal
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.

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) == 5
E assert 6 == 5
E + 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 :

Fenêtre de terminal
pytest -v # détaille chaque test (verbose)
pytest -q # sortie compacte
pytest test_calculs.py # un seul fichier
pytest -k addition # seulement les tests dont le nom contient "addition"
pytest -x # s'arrête au premier échec

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) == attendu

pytest compte alors trois tests distincts, un par ligne, et nomme chacun par ses valeurs :

test_calculs.py::test_addition[2-3-5] PASSED
test_calculs.py::test_addition[-1-1-0] PASSED
test_calculs.py::test_addition[0-0-0] PASSED

Si un cas échoue, vous savez immédiatement lequel. C'est la façon idiomatique de couvrir les cas limites sans dupliquer le code.

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.fixture
def 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.

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)

La couverture indique quelle part de votre code est réellement exécutée par les tests. Le plugin pytest-cov l'ajoute à pytest :

Fenêtre de terminal
pip install pytest-cov
pytest --cov=calculs --cov-report=term-missing
Name 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.

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.

  • pytest teste avec une simple fonction test_* et le mot-clé assert, sans classe.
  • La commande pytest dé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.parametrize rejoue un test sur plusieurs cas sans le dupliquer.
  • Une fixture prépare un contexte réutilisable et indépendant entre tests.
  • pytest.raises vérifie les exceptions, pytest.approx compare les flottants.
  • pytest-cov mesure la couverture, mais 100 % ne garantit pas l'absence de bugs.
  • pytest exécute aussi les tests unittest : la migration peut être progressive.

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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn