Les tests unitaires modernes avec Pytest#
Bien que le module natif unittest soit très complet, l'écosystème professionnel Python plébiscite massivement pytest comme le standard absolu de l'industrie.
Pourquoi pytest s'est-il imposé ?
1. Zéro cérémonial (Boilerplate minimal) : pas besoin de créer des classes complexes héritant de TestCase. De simples fonctions Python autonomes préfixées par test_ suffisent.
2. L'instruction native assert : fini les dizaines de méthodes self.assertEqual(), self.assertIn(). Vous utilisez l'instruction Python ordinaire assert condition.
3. Introspection détaillée des échecs : lorsqu'un test échoue, pytest dissèque l'expression et affiche exactement les valeurs des variables en jeu.
4. Le système révolutionnaire de Fixtures (@pytest.fixture) et la paramétrisation (@pytest.mark.parametrize).
1. Installation et premier test avec pytest#
Pytest s'installe via le gestionnaire de paquets officiel pip :
Écriture d'un test minimal#
def carre(x: int) -> int:
return x * x
def test_carre_nombre_positif():
assert carre(4) == 16
def test_carre_nombre_negatif():
assert carre(-3) == 9
def test_carre_zero():
assert carre(0) == 0
Pour exécuter les tests, tapez simplement dans votre terminal :
2. Tester les exceptions : pytest.raises()#
Pour vérifier qu'une fonction lève l'exception attendue face à des entrées invalides :
import pytest
def diviser(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("Le denominateur ne peut pas etre nul !")
return a / b
def test_division_par_zero():
# Vérifie que l'erreur ZeroDivisionError est bien déclenchée
with pytest.raises(ZeroDivisionError) as info_erreur:
diviser(10, 0)
# On peut même vérifier le message de l'exception
assert "denominateur" in str(info_erreur.value)
3. Les Fixtures de test : @pytest.fixture#
Une fixture est une fonction décorée qui prépare un environnement ou un jeu de données (connexion à une base de test, objet initialisé, fichier temporaire) et l'injecte automatiquement en paramètre des fonctions de test qui la demandent :
import pytest
class PanierAchat:
def __init__(self):
self.articles = []
def ajouter(self, nom: str, prix: float):
self.articles.append({"nom": nom, "prix": prix})
@property
def total(self) -> float:
return sum(item["prix"] for item in self.articles)
# Définition de la Fixture
@pytest.fixture
def panier_garni():
"""Prépare un panier contenant déjà deux articles de référence."""
panier = PanierAchat()
panier.ajouter("Livre Python", 29.90)
panier.ajouter("Stylo", 2.10)
return panier
# Utilisation de la fixture : elle est passée comme argument du test !
def test_total_panier_initial(panier_garni):
assert panier_garni.total == 32.00
def test_ajout_article_augmente_total(panier_garni):
panier_garni.ajouter("Clavier", 48.00)
assert panier_garni.total == 80.00
assert len(panier_garni.articles) == 3
4. Paramétrisation de tests : @pytest.mark.parametrize#
Plutôt que d'écrire dix fonctions de test presque identiques pour tester différents cas limites, pytest permet d'exécuter un test unique sur une matrice complète d'entrées et de sorties :
import pytest
def est_palindrome(chaine: str) -> bool:
s = chaine.lower().replace(" ", "")
return s == s[::-1]
# Exécute 5 tests unitaires distincts en 4 lignes de code !
@pytest.mark.parametrize("texte, attendu", [
("radar", True),
("kayak", True),
("Python", False),
("Elu par cette crapule", True),
("", True),
])
def test_palindromes(texte, attendu):
assert est_palindrome(texte) == attendu
5. Synthèse comparative : unittest vs pytest#
| Fonctionnalité | unittest (Standard) |
pytest (Standard Industrie) |
|---|---|---|
| Installation | Aucune (Inclus avec Python) | pip install pytest |
| Structure du code | Classes obligatoires (TestCase) |
Fonctions légères et expressives |
| Syntaxe d'assertion | Verbeuse (self.assertEqual(a, b)) |
Naturelle et concise (assert a == b) |
| Préparation (Fixtures) | setUp() / tearDown() rigides |
Fixtures modulaires injectables (@pytest.fixture) |
| Tests paramétrés | Complexe ou via sous-tests | Élégant (@pytest.mark.parametrize) |
| Rapports d'erreurs | Basiques | Visuels avec valeurs des variables en panne |
6. Défi d'application#
À l'aide de @pytest.mark.parametrize, écrivez une suite de tests unitaires pour une fonction valider_code_postal(code: str) -> bool qui vérifie qu'un code postal français comporte exactement 5 chiffres numériques (ex: "75001" \(\rightarrow\) True, "7500" \(\rightarrow\) False, "7500A" \(\rightarrow\) False, "" \(\rightarrow\) False).
Commentaires