Guide : Combinaison de pytest et Design by Contract (DbC) avec deal¶
Pour les étudiants en programmation orientée objet
1. Introduction¶
Le Design by Contract (DbC) et les tests unitaires sont deux approches complémentaires pour assurer la qualité du code :
- DbC (
deal) : Vérifie les préconditions, postconditions et invariants pendant l’exécution. - Tests unitaires (
pytest) : Vérifient le comportement attendu de manière indépendante.
Quand utiliser quoi ?¶
| Approche | Utilisation | Exemple |
|---|---|---|
| DbC | Vérifications runtime (ex: solde ≥ 0 dans un compte bancaire). | @inv(lambda self: self.solde >= 0) |
| Tests unitaires | Vérifications logiques (ex: “Un retrait de 50€ doit réduire le solde de 50€”). | assert compte.solde == 150 après un dépôt de 50€ |
2. Ce que les tests doivent vérifier¶
Ce qui est déjà couvert par DbC (à ne pas retester)¶
❌ Ne testez pas :
- Les préconditions (
@pre) →deals’en charge. - Les invariants (
@inv) →deals’en charge. - Les postconditions (
@post) → Si elles échouent, c’est une erreur de programmation.
Ce que les tests doivent vérifier¶
✅ Testez :
- Le comportement logique (ex: “Un dépôt augmente le solde”).
- Les cas limites (ex: “Un retrait avec un solde insuffisant lève une exception”).
- L’intégration entre méthodes (ex: “Après un dépôt, un retrait fonctionne correctement”).
3. Exemple complet : CompteBancaire avec DbC + pytest¶
Code source (avec deal)¶
from deal import pre, post, inv
@inv(lambda self: self._solde >= 0) # Invariant : solde toujours ≥ 0
class CompteBancaire:
def __init__(self, titulaire: str, solde_initial: float = 0.0):
self.titulaire = titulaire
self._solde = solde_initial
@pre(lambda montant: montant > 0) # Précondition : montant positif
def deposer(self, montant: float):
"""Dépose un montant sur le compte."""
self._solde += montant
@pre(lambda montant: montant > 0 and montant <= self._solde) # Précondition
def retirer(self, montant: float):
"""Retire un montant du compte. Lève ValueError si solde insuffisant."""
if montant > self._solde:
raise ValueError("Solde insuffisant")
self._solde -= montant
@post(lambda result: isinstance(result, (int, float)) and result >= 0) # Postcondition
def get_solde(self) -> float:
"""Retourne le solde actuel."""
return self._solde
Tests pytest associés¶
import pytest
from votre_module import CompteBancaire
def test_deposer_montant_valide():
compte = CompteBancaire("Alice", 100.0)
compte.deposer(50.0) # Précondition vérifiée par `deal`
assert compte.get_solde() == 150.0 # Test du comportement logique
def test_retirer_montant_valide():
compte = CompteBancaire("Bob", 200.0)
compte.retirer(75.0) # Précondition vérifiée par `deal`
assert compte.get_solde() == 125.0 # Test du comportement logique
def test_retirer_solde_insuffisant():
compte = CompteBancaire("Charlie", 50.0)
with pytest.raises(ValueError): # Test de l'exception
compte.retirer(60.0) # Précondition échoue → `deal` lève AssertionError
def test_invariant_solde_negatif():
compte = CompteBancaire("Dave", 100.0)
with pytest.raises(AssertionError): # Test de l'invariant
compte._solde = -50.0 # `deal` lève AssertionError
4. Stratégie de test avec DbC¶
Étape 1 : Tester les cas valides (comportement logique)¶
def test_deposer_et_retirer():
compte = CompteBancaire("Alice", 100.0)
compte.deposer(50.0) # OK (précondition vérifiée par `deal`)
compte.retirer(30.0) # OK (précondition vérifiée par `deal`)
assert compte.get_solde() == 120.0 # Test du résultat final
Étape 2 : Tester les cas invalides (exceptions)¶
def test_deposer_montant_negatif():
compte = CompteBancaire("Bob", 100.0)
with pytest.raises(AssertionError): # `deal` lève AssertionError
compte.deposer(-50.0) # Précondition échoue
def test_retirer_solde_insuffisant():
compte = CompteBancaire("Charlie", 50.0)
with pytest.raises(ValueError): # Exception levée par la méthode
compte.retirer(60.0) # Précondition échoue → `deal` lève AssertionError
Étape 3 : Tester les invariants (modifications directes)¶
def test_invariant_violé():
compte = CompteBancaire("Dave", 100.0)
with pytest.raises(AssertionError): # `deal` lève AssertionError
compte._solde = -50.0 # Accès direct à l'attribut (interdit par `@inv`)
5. Bonnes pratiques¶
✅ Ce que les tests doivent faire¶
- Vérifier le comportement attendu (ex: “Un dépôt augmente le solde”).
- Tester les exceptions (ex: “Un retrait avec solde insuffisant lève une erreur”).
- Valider l’intégration entre méthodes (ex: “Après un dépôt, un retrait fonctionne”).
❌ Ce que les tests ne doivent pas faire¶
- Retester les préconditions/postconditions → C’est le rôle de
deal. - Tester les invariants manuellement →
deals’en charge. - Répéter les vérifications de type → Utilisez des annotations (
: float) +mypy.
6. Exercice pour les étudiants¶
Consigne¶
Écrivez des tests pytest pour la classe Rectangle suivante (avec DbC) :
from deal import pre, post, inv
@inv(lambda self: self.largeur > 0 and self.hauteur > 0)
class Rectangle:
def __init__(self, largeur: float, hauteur: float):
self.largeur = largeur
self.hauteur = hauteur
@pre(lambda valeur: valeur > 0)
def set_largeur(self, valeur: float):
self.largeur = valeur
@pre(lambda valeur: valeur > 0)
def set_hauteur(self, valeur: float):
self.hauteur = valeur
@post(lambda aire: aire > 0)
def calculer_aire(self) -> float:
return self.largeur * self.hauteur
Tests attendus¶
- Test de la création d’un rectangle valide.
- Test de l’échec d’une largeur/hauteur négative (précondition).
- Test du calcul de l’aire.
- Test de l’invariant (modification directe des attributs).
7. Ressources supplémentaires¶
Résumé final¶
| Approche | Rôle | Exemple |
|---|---|---|
DbC (deal) |
Vérifie les contrats (pré/postconditions, invariants). | @pre(lambda x: x > 0) |
Tests unitaires (pytest) |
Vérifie le comportement logique et les cas d’usage. | assert compte.solde == 150 |
Stratégie :
- Laissez
dealgérer les vérifications de contrat. - Utilisez
pytestpour tester la logique métier et les interactions entre méthodes.