Module : Les Tests avec Pytest
Objectif
Ce module a pour but de vous initier à pytest, le framework de test le plus populaire et le plus puissant de l'écosystème Python. Vous apprendrez à écrire des tests simples, à utiliser les fixtures pour préparer le contexte de vos tests, et à comprendre la philosophie de pytest qui privilégie la simplicité et la lisibilité.
1. Pourquoi Écrire des Tests ?
Les tests automatisés sont une partie cruciale du développement logiciel moderne. Ils permettent de :
- Vérifier la correction : S'assurer que votre code fait ce qu'il est censé faire.
- Prévenir les régressions : Garantir que les nouvelles modifications n'ont pas cassé des fonctionnalités existantes.
- Faciliter le refactoring : Vous pouvez modifier votre code en toute confiance, sachant que la suite de tests vous alertera si quelque chose ne va plus.
- Servir de documentation : Les tests montrent comment votre code est censé être utilisé.
2. pytest vs unittest
unittest est le framework de test inclus dans la bibliothèque standard de Python. pytest est un package tiers qui s'est imposé comme le standard de facto grâce à ses nombreux avantages :
- Syntaxe plus simple : Pas besoin de créer des classes qui héritent de
unittest.TestCase. De simples fonctions suffisent. - Assertions plus claires : On utilise l'instruction
assertstandard de Python.pytestse charge de fournir des messages d'erreur détaillés en cas d'échec. - Découverte de tests puissante :
pytesttrouve et exécute automatiquement vos tests si vous suivez des conventions de nommage simples. - Fixtures : Un système d'injection de dépendances élégant et puissant pour gérer l'état et le contexte des tests.
- Écosystème de plugins riche : Des centaines de plugins pour étendre les fonctionnalités de
pytest(tests de couverture, tests asynchrones, intégration avec Django/Flask, etc.).
3. Écrire son Premier Test avec pytest
-
Installation :
pip install pytest -
Conventions de nommage :
- Les fichiers de test doivent être nommés
test_*.pyou*_test.py. - Les fonctions de test à l'intérieur de ces fichiers doivent être nommées
test_*(). - Les classes de test (optionnelles) doivent être nommées
Test*.
- Les fichiers de test doivent être nommés
-
Exemple : Créez un fichier
calculs.pyavec une fonction à tester :# calculs.py
def addition(a, b):
return a + bCréez un fichier de test
test_calculs.pydans le même dossier :# test_calculs.py
from calculs import addition
# Une simple fonction de test
def test_addition():
# L'assertion est simple et directe
assert addition(2, 3) == 5
assert addition(-1, 1) == 0
assert addition(-1, -1) == -2 -
Lancer les tests : Ouvrez un terminal dans le dossier et lancez simplement la commande
pytest:pytest -
Résultat :
============================= test session starts ==============================
...
collected 1 item
test_calculs.py . [100%]
============================== 1 passed in ...s ===============================Le
.indique qu'un test a réussi. Si un test échoue, vous verrez unF.
Exemple d'un test qui échoue
Modifions le test pour qu'il échoue :
# test_calculs.py
def test_addition_echec():
assert addition(2, 3) == 6 # Assertion incorrecte
pytest nous donne un rapport d'erreur très détaillé :
...
def test_addition_echec():
> assert addition(2, 3) == 6
E assert 5 == 6
E + where 5 = addition(2, 3)
test_calculs.py:10: AssertionError
=========================== 1 failed, 1 passed in ...s ===========================
pytest a ré-écrit l'assertion pour nous montrer exactement quelles valeurs ont été comparées. C'est beaucoup plus informatif qu'un simple AssertionError.
4. Les Fixtures : Gérer le Contexte des Tests
Les fixtures sont le cœur de la puissance de pytest. Une fixture est une fonction qui prépare des données ou un état nécessaire pour un test. pytest les exécute avant le test qui en a besoin et "injecte" leur résultat dans le test.
On déclare une fixture avec le décorateur @pytest.fixture.
Exemple simple de fixture
Imaginons que plusieurs tests ont besoin d'une liste de nombres de départ.
# test_liste.py
import pytest
@pytest.fixture
def liste_nombres():
"""Une fixture qui fournit une liste de nombres pour les tests."""
print("\n(Création de la liste via la fixture)")
return [1, 2, 3, 4, 5]
def test_longueur_liste(liste_nombres):
"""Teste la longueur de la liste fournie par la fixture."""
# 'liste_nombres' est le résultat de la fixture
assert len(liste_nombres) == 5
def test_somme_liste(liste_nombres):
"""Teste la somme de la liste fournie par la fixture."""
assert sum(liste_nombres) == 15
- Pour utiliser une fixture, on l'ajoute simplement comme argument à la fonction de test.
pytestvoit l'argument, cherche une fixture avec le même nom, l'exécute, et passe sa valeur de retour au test.- Par défaut, la fixture est exécutée une fois pour chaque test qui l'utilise, garantissant l'isolation des tests.
Portée des Fixtures (scope)
On peut contrôler la fréquence d'exécution d'une fixture avec l'argument scope.
scope='function'(défaut) : Exécutée une fois par test.scope='class': Exécutée une fois par classe de test.scope='module': Exécutée une fois par fichier de test.scope='session': Exécutée une fois pour toute la session de test.
C'est utile pour des opérations de configuration coûteuses (ex: connexion à une base de données, création d'un gros objet).
@pytest.fixture(scope='module')
def connexion_db():
"""Fixture coûteuse, exécutée une seule fois pour tout le module."""
print("\n(Connexion à la base de données...)")
db = ... # Code de connexion
yield db # 'yield' est utilisé pour le nettoyage
print("\n(Fermeture de la connexion à la base de données...)")
db.close()
L'utilisation de yield dans une fixture permet de séparer la partie "setup" (avant yield) de la partie "teardown" (après yield), qui sera exécutée à la fin de la portée de la fixture.
5. Paramétrer les Tests
Le décorateur @pytest.mark.parametrize permet d'exécuter le même test avec plusieurs jeux de données différents.
import pytest
@pytest.mark.parametrize("a, b, attendu", [
(2, 3, 5), # Test 1
(-1, 1, 0), # Test 2
(0, 0, 0), # Test 3
(-5, -5, -10), # Test 4
])
def test_addition_parametrise(a, b, attendu):
assert addition(a, b) == attendu
Ce code définit un seul test, mais pytest l'exécutera quatre fois, une pour chaque tuple de données. C'est une manière très propre et lisible de tester de nombreux cas.
6. Quelques Fonctionnalités Utiles
-
Tester les exceptions :
def test_division_par_zero():
with pytest.raises(ZeroDivisionError):
1 / 0Le test réussit si une
ZeroDivisionErrorest levée dans le blocwith. -
Marqueurs (
markers) : Organiser les tests en catégories.@pytest.mark.slow
def test_tres_lent():
...
@pytest.mark.network
def test_appel_api():
...On peut ensuite lancer uniquement une catégorie de tests :
pytest -m slow. -
Fichier
conftest.py: Un fichier spécial où l'on peut placer des fixtures pour qu'elles soient disponibles pour tous les tests d'un dossier et de ses sous-dossiers.
Conclusion
pytest est un outil qui transforme l'écriture de tests en une tâche agréable et productive. En utilisant des fonctions de test simples, des assertions claires, et le puissant système de fixtures, vous pouvez construire des suites de tests robustes et maintenables. L'investissement dans l'apprentissage de pytest est l'un des plus rentables pour un développeur Python souhaitant améliorer la qualité de son code.