Définition
Écrire du code et espérer qu'il marche ne suffit pas longtemps. pytest le vérifie à votre place et nomme ce qui échoue.
Un test, ici, est une simple fonction dont le nom commence par test_, et qui contient au moins un assert. Aucune classe à créer, aucune méthode spéciale à appeler : la fonction est le test.
Un exemple, sur un panier qui additionne des prix.
# fichier test_panier.py
from panier import total
def test_additionne_les_prix():
assert total([2.5, 3.5]) == 6.0
def test_panier_vide_vaut_zero():
assert total([]) == 0La commande se lance depuis la racine du projet, sans argument : l'outil cherche les fichiers test_*.py, les importe, appelle les fonctions test*, et détaille chaque échec.
pytestpytest ne fait pas partie de la bibliothèque standard : il s'installe avec pip, de préférence dans un environnement virtuel propre au projet.
Le problème qu'il résout
Sans outil de test, vérifier qu'un programme marche encore après une modification revient à le relancer à la main et à lire ce qu'affiche un print posé ici ou là. La méthode tient jusqu'à deux cents lignes, le temps qu'un projet reste petit.
Au-delà, plus personne ne rejoue de mémoire les quinze cas limites du mois dernier, et une correction en casse une autre sans que rien ne le signale.
Un test automatisé fige la vérification. La question posée après chaque changement n'est plus « est-ce que ça marche encore », répondue de mémoire, mais « qu'est-ce qui a cessé de marcher », à laquelle l'outil répond en nommant le fichier, la ligne et la valeur obtenue.
Face à unittest
Python embarque déjà unittest, un module de tests calqué sur JUnit. Voici ce qu'on gagne, et ce qu'on perd, à en installer un second.
| Sur ce point | unittest | pytest |
|---|---|---|
| Installation | Inclus dans Python | À installer |
| Écrire un test | Une méthode dans une classe | Une fonction libre |
| Vérifier | Une trentaine de méthodes assertX | Le mot-clé assert seul |
| Préparer les données | setUp, avant chaque test | Des fixtures réclamées par leur nom |
| Message d'échec | Les deux valeurs si la bonne méthode a été choisie | Les deux valeurs, toujours |
La dernière ligne fait basculer la plupart des projets : avec pytest, un assert resultat == attendu qui échoue suffit, l'outil réécrit l'expression et affiche les deux côtés. Avec unittest, il aurait fallu deviner la bonne méthode assertX parmi la trentaine disponible.
Dans l'autre sens, unittest garde un avantage réel : aucune dépendance à installer, ce qui compte pour une bibliothèque distribuée. pytest exécute d'ailleurs les tests écrits pour unittest, donc la bascule se fait fichier par fichier.
Le piège du premier jour
Le préfixe test_ sur le fichier et sur la fonction n'est pas une question de style : c'est le mécanisme par lequel pytest repère ce qu'il doit exécuter.
Un fichier appelé panier_test.py ou une fonction appelée verifie_total ne seront jamais exécutés. L'outil annonce collected 0 items, sort en une fraction de seconde et ne signale aucun échec. Rien n'est passé au vert : rien n'a été lancé, parfois pendant des semaines.
Le second contretemps est une ModuleNotFoundError sur son propre code, alors que le fichier se trouve juste à côté. pytest ajoute au chemin d'import la racine détectée, pas le dossier courant : lancer la commande depuis la racine du projet règle la majorité des cas, et installer le projet en mode modifiable règle les autres.
Fixtures, paramétrage et exceptions attendues
Trois besoins reviennent souvent. Le premier : une donnée que plusieurs tests partagent, comme un panier déjà rempli, déclarée une fois comme fixture avec un décorateur et réclamée par chaque test qui la nomme parmi ses arguments.
Le deuxième : une vérification à rejouer sur plusieurs valeurs, sans dupliquer la fonction, grâce au paramétrage, chaque valeur comptant comme un test à part entière.
Le troisième : vérifier qu'une exception est bien levée, via un gestionnaire de contexte dédié qui échoue si rien ne s'est produit à l'intérieur.
Les trois, réunis dans un même fichier.
import pytest
from panier import total
@pytest.fixture
def panier_type():
return [2.5, 3.5, 4.0]
def test_total(panier_type):
assert total(panier_type) == 10.0
@pytest.mark.parametrize("prix", [-1, -0.5])
def test_refuse_un_prix_negatif(prix):
with pytest.raises(ValueError):
total([prix])Ce qu'il ne prouve pas
Une suite verte ne dit pas que le code est correct. Elle dit seulement que les cas écrits se comportent comme prévu, ce qui est très différent.
L'erreur classique consiste à tester le langage plutôt que sa propre logique : vérifier qu'une addition donne le bon total, ou courir après un pourcentage de couverture. Ces tests passent toujours, parce qu'ils portent sur un comportement que Python garantit déjà, et ne détectent donc jamais rien.
Un test utile porte sur une règle qui pourrait changer par accident : une remise plafonnée, un arrondi, un refus attendu. Et pytest ne remplace ni ruff, qui relit la forme du code, ni mypy, qui vérifie la cohérence des types sans rien exécuter. Les trois attrapent des défauts de nature différente, complémentaires plutôt que redondants.
Questions fréquentes
Faut-il installer pytest alors que Python contient déjà unittest ?
Dans la plupart des projets, oui. La syntaxe plus courte fait qu'on écrit davantage de tests, et c'est le nombre de tests réellement écrits qui protège, jamais leur élégance. La vraie exception est la bibliothèque distribuée, où unittest garde l'avantage de n'imposer aucune dépendance supplémentaire.
Combien de tests faut-il écrire sur un projet qui démarre ?
Moins qu'on ne l'imagine au départ. Un test sur le calcul qui porte la valeur du produit, puis un test par bug corrigé, celui qui reproduit le bug avant même la correction. Cette règle construit une suite utile sans jamais y consacrer de session dédiée.
Comment lancer un seul test au lieu de toute la suite ?
En faisant suivre le chemin du fichier de :: et du nom de la fonction, par exemple pytest tests/test_panier.py::test_total. L'option -k filtre par fragment de nom et -x arrête tout au premier échec, ce qui évite de lire cinquante erreurs venues d'une seule cause.