Les tests unitaires, c’est ce qui t’évite de casser ton code sans t’en rendre compte. Dans cet article, on va voir pourquoi ils sont indispensables en Python et comment en écrire simplement, étape par étape, pour rendre ton code plus robuste et plus agréable à maintenir.
Pourquoi écrire des tests ?
Le problème sans tests
Imaginez que vous développez une application bancaire :
# banking.py
class CompteBancaire:
def __init__(self, solde_initial=0):
self.solde = solde_initial
def deposer(self, montant):
self.solde += montant
def retirer(self, montant):
self.solde -= montant # ❌ Bug : pas de vérification du solde !
def transferer(self, autre_compte, montant):
self.retirer(montant)
autre_compte.deposer(montant)
# Utilisation sans tests
compte = CompteBancaire(100)
compte.retirer(150) # Solde devient -50 ! 😱
print(f"Solde : {compte.solde}€") # -50€
Les problèmes :
-
Aucune garantie que le code fonctionne correctement
-
Les bugs ne sont découverts qu'en production
-
Difficile de savoir ce qui casse après une modification
-
Pas de documentation sur le comportement attendu
La solution : les tests automatisés
# test_banking.py
import unittest
from banking import CompteBancaire
class TestCompteBancaire(unittest.TestCase):
def test_retrait_avec_solde_insuffisant(self):
"""Vérifie qu'on ne peut pas retirer plus que le solde."""
compte = CompteBancaire(100)
with self.assertRaises(ValueError):
compte.retirer(150)
if __name__ == '__main__':
unittest.main()
Les avantages :
-
Confiance : vous savez que votre code fonctionne
-
Documentation : les tests montrent comment utiliser le code
-
Refactoring sûr : vous pouvez modifier sans casser
-
Détection précoce : les bugs sont trouvés immédiatement
-
Régression : évite que d'anciens bugs réapparaissent
Les bases avec unittest
Python inclut le module unittest dans sa bibliothèque standard.
Votre premier test
# calculatrice.py
def additionner(a, b):
"""Additionne deux nombres."""
return a + b
def soustraire(a, b):
"""Soustrait b de a."""
return a - b
def multiplier(a, b):
"""Multiplie deux nombres."""
return a * b
def diviser(a, b):
"""Divise a par b."""
if b == 0:
raise ValueError("Division par zéro impossible")
return a / b
# test_calculatrice.py
import unittest
from calculatrice import additionner, soustraire, multiplier, diviser
class TestCalculatrice(unittest.TestCase):
"""Tests pour les fonctions de calculatrice."""
def test_additionner(self):
"""Test de l'addition."""
self.assertEqual(additionner(2, 3), 5)
self.assertEqual(additionner(-1, 1), 0)
self.assertEqual(additionner(0, 0), 0)
def test_soustraire(self):
"""Test de la soustraction."""
self.assertEqual(soustraire(5, 3), 2)
self.assertEqual(soustraire(0, 5), -5)
def test_multiplier(self):
"""Test de la multiplication."""
self.assertEqual(multiplier(3, 4), 12)
self.assertEqual(multiplier(-2, 3), -6)
self.assertEqual(multiplier(0, 100), 0)
def test_diviser(self):
"""Test de la division."""
self.assertEqual(diviser(10, 2), 5)
self.assertAlmostEqual(diviser(1, 3), 0.333, places=3)
def test_diviser_par_zero(self):
"""Test de la division par zéro."""
with self.assertRaises(ValueError):
diviser(10, 0)
if __name__ == '__main__':
unittest.main()
Exécution :
python test_calculatrice.py
Résultat :
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
Comprendre la structure
import unittest
# 1. Créer une classe de test héritant de unittest.TestCase
class TestMaFonction(unittest.TestCase):
# 2. Chaque méthode de test commence par "test_"
def test_cas_normal(self):
"""Description du test."""
# 3. Préparer (Arrange)
valeur = 42
# 4. Exécuter (Act)
resultat = ma_fonction(valeur)
# 5. Vérifier (Assert)
self.assertEqual(resultat, valeur_attendue)
Le pattern AAA (Arrange, Act, Assert) :
-
Arrange : préparer les données et l'environnement
-
Act : exécuter le code à tester
-
Assert : vérifier que le résultat est correct
Les assertions essentielles
Assertions d'égalité
class TestAssertions(unittest.TestCase):
def test_egalite(self):
"""Test d'égalité basique."""
self.assertEqual(2 + 2, 4)
self.assertEqual("hello", "hello")
self.assertEqual([1, 2, 3], [1, 2, 3])
def test_inegalite(self):
"""Test d'inégalité."""
self.assertNotEqual(5, 3)
self.assertNotEqual("chat", "chien")
def test_egalite_approximative(self):
"""Pour les nombres flottants."""
self.assertAlmostEqual(0.1 + 0.2, 0.3, places=7)
self.assertAlmostEqual(1/3, 0.333, places=3)
Assertions booléennes
class TestBooleens(unittest.TestCase):
def test_verite(self):
"""Test de vérité."""
self.assertTrue(True)
self.assertTrue(1 == 1)
self.assertTrue("texte") # Valeur truthy
def test_faussete(self):
"""Test de fausseté."""
self.assertFalse(False)
self.assertFalse(0 == 1)
self.assertFalse("") # Valeur falsy
def test_is(self):
"""Test d'identité."""
a = [1, 2, 3]
b = a
self.assertIs(a, b) # Même objet en mémoire
c = [1, 2, 3]
self.assertIsNot(a, c) # Objets différents
def test_none(self):
"""Test de None."""
valeur = None
self.assertIsNone(valeur)
valeur = "quelque chose"
self.assertIsNotNone(valeur)
Assertions de conteneurs
class TestConteneurs(unittest.TestCase):
def test_in(self):
"""Test d'appartenance."""
self.assertIn(3, [1, 2, 3, 4])
self.assertIn('a', 'chaîne')
self.assertIn('key', {'key': 'value'})
def test_not_in(self):
"""Test de non-appartenance."""
self.assertNotIn(5, [1, 2, 3])
self.assertNotIn('z', 'chaîne')
def test_count_equal(self):
"""Vérifie que deux séquences ont les mêmes éléments."""
self.assertCountEqual([1, 2, 3], [3, 2, 1]) # Ordre importe pas
self.assertCountEqual("abc", "bca")
Assertions de types
class TestTypes(unittest.TestCase):
def test_isinstance(self):
"""Test de type."""
self.assertIsInstance("texte", str)
self.assertIsInstance(42, int)
self.assertIsInstance([1, 2], list)
self.assertNotIsInstance("42", int)
Assertions d'exceptions
class TestExceptions(unittest.TestCase):
def test_raises_simple(self):
"""Test qu'une exception est levée."""
def diviser_par_zero():
return 1 / 0
self.assertRaises(ZeroDivisionError, diviser_par_zero)
def test_raises_with_context(self):
"""Test avec gestionnaire de contexte."""
with self.assertRaises(ValueError):
int("pas un nombre")
def test_raises_avec_message(self):
"""Vérifier le message de l'exception."""
with self.assertRaises(ValueError) as context:
raise ValueError("Message d'erreur spécifique")
self.assertIn("spécifique", str(context.exception))
def test_raises_regex(self):
"""Vérifier le message avec une regex."""
with self.assertRaisesRegex(ValueError, r'\d+ est invalide'):
raise ValueError("42 est invalide")
setUp et tearDown : préparer et nettoyer
Méthodes de configuration
class TestAvecSetup(unittest.TestCase):
def setUp(self):
"""
Exécuté AVANT chaque test.
Utilisé pour préparer l'environnement.
"""
print("setUp : préparation")
self.liste = [1, 2, 3, 4, 5]
self.compte = CompteBancaire(1000)
def tearDown(self):
"""
Exécuté APRÈS chaque test.
Utilisé pour nettoyer.
"""
print("tearDown : nettoyage")
self.liste = None
self.compte = None
def test_premier(self):
"""Premier test."""
print(" test_premier")
self.assertEqual(len(self.liste), 5)
def test_deuxieme(self):
"""Deuxième test."""
print(" test_deuxieme")
self.assertEqual(self.compte.solde, 1000)
# Résultat :
# setUp : préparation
# test_premier
# tearDown : nettoyage
# setUp : préparation
# test_deuxieme
# tearDown : nettoyage
Configuration de classe
class TestAvecSetupClass(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""
Exécuté UNE SEULE FOIS avant tous les tests.
Pour les opérations coûteuses.
"""
print("setUpClass : initialisation de la classe")
cls.connexion_db = "connexion simulée"
@classmethod
def tearDownClass(cls):
"""
Exécuté UNE SEULE FOIS après tous les tests.
Pour nettoyer les ressources partagées.
"""
print("tearDownClass : fermeture connexion")
cls.connexion_db = None
def setUp(self):
"""Exécuté avant chaque test."""
print(" setUp : préparation test")
def test_un(self):
print(" test_un")
self.assertIsNotNone(self.connexion_db)
def test_deux(self):
print(" test_deux")
self.assertIsNotNone(self.connexion_db)
Tests de classes : exemple complet
# banking.py (version corrigée)
class CompteBancaire:
"""Représente un compte bancaire."""
def __init__(self, titulaire, solde_initial=0):
if solde_initial < 0:
raise ValueError("Le solde initial ne peut pas être négatif")
self.titulaire = titulaire
self.solde = solde_initial
self.historique = []
def deposer(self, montant):
"""Dépose de l'argent sur le compte."""
if montant <= 0:
raise ValueError("Le montant doit être positif")
self.solde += montant
self.historique.append(f"Dépôt: +{montant}€")
return self.solde
def retirer(self, montant):
"""Retire de l'argent du compte."""
if montant <= 0:
raise ValueError("Le montant doit être positif")
if montant > self.solde:
raise ValueError("Solde insuffisant")
self.solde -= montant
self.historique.append(f"Retrait: -{montant}€")
return self.solde
def transferer(self, autre_compte, montant):
"""Transfère de l'argent vers un autre compte."""
self.retirer(montant)
autre_compte.deposer(montant)
return self.solde
def obtenir_historique(self):
"""Retourne l'historique des transactions."""
return self.historique.copy()
# test_banking.py
import unittest
from banking import CompteBancaire
class TestCompteBancaire(unittest.TestCase):
"""Suite de tests pour CompteBancaire."""
def setUp(self):
"""Prépare un compte pour chaque test."""
self.compte = CompteBancaire("Alice", 1000)
# Tests du constructeur
def test_creation_compte_valide(self):
"""Test de création d'un compte valide."""
compte = CompteBancaire("Bob", 500)
self.assertEqual(compte.titulaire, "Bob")
self.assertEqual(compte.solde, 500)
self.assertEqual(len(compte.historique), 0)
def test_creation_compte_sans_solde(self):
"""Test de création sans solde initial."""
compte = CompteBancaire("Charlie")
self.assertEqual(compte.solde, 0)
def test_creation_solde_negatif(self):
"""Test qu'on ne peut pas créer un compte avec solde négatif."""
with self.assertRaises(ValueError):
CompteBancaire("Dave", -100)
# Tests de dépôt
def test_depot_valide(self):
"""Test d'un dépôt valide."""
nouveau_solde = self.compte.deposer(500)
self.assertEqual(nouveau_solde, 1500)
self.assertEqual(self.compte.solde, 1500)
def test_depot_negatif(self):
"""Test qu'on ne peut pas déposer un montant négatif."""
with self.assertRaises(ValueError):
self.compte.deposer(-100)
def test_depot_zero(self):
"""Test qu'on ne peut pas déposer zéro."""
with self.assertRaises(ValueError):
self.compte.deposer(0)
def test_depot_historique(self):
"""Test que le dépôt est ajouté à l'historique."""
self.compte.deposer(200)
historique = self.compte.obtenir_historique()
self.assertEqual(len(historique), 1)
self.assertIn("Dépôt: +200€", historique)
# Tests de retrait
def test_retrait_valide(self):
"""Test d'un retrait valide."""
nouveau_solde = self.compte.retirer(300)
self.assertEqual(nouveau_solde, 700)
self.assertEqual(self.compte.solde, 700)
def test_retrait_solde_insuffisant(self):
"""Test qu'on ne peut pas retirer plus que le solde."""
with self.assertRaises(ValueError) as context:
self.compte.retirer(2000)
self.assertIn("insuffisant", str(context.exception).lower())
self.assertEqual(self.compte.solde, 1000) # Solde inchangé
def test_retrait_negatif(self):
"""Test qu'on ne peut pas retirer un montant négatif."""
with self.assertRaises(ValueError):
self.compte.retirer(-50)
def test_retrait_historique(self):
"""Test que le retrait est ajouté à l'historique."""
self.compte.retirer(100)
historique = self.compte.obtenir_historique()
self.assertIn("Retrait: -100€", historique)
# Tests de transfert
def test_transfert_valide(self):
"""Test d'un transfert valide."""
compte_bob = CompteBancaire("Bob", 500)
self.compte.transferer(compte_bob, 200)
self.assertEqual(self.compte.solde, 800)
self.assertEqual(compte_bob.solde, 700)
def test_transfert_solde_insuffisant(self):
"""Test d'un transfert avec solde insuffisant."""
compte_bob = CompteBancaire("Bob", 500)
with self.assertRaises(ValueError):
self.compte.transferer(compte_bob, 2000)
# Vérifier qu'aucun compte n'a été modifié
self.assertEqual(self.compte.solde, 1000)
self.assertEqual(compte_bob.solde, 500)
def test_transfert_historiques(self):
"""Test que le transfert apparaît dans les deux historiques."""
compte_bob = CompteBancaire("Bob", 500)
self.compte.transferer(compte_bob, 100)
historique_alice = self.compte.obtenir_historique()
historique_bob = compte_bob.obtenir_historique()
self.assertEqual(len(historique_alice), 1)
self.assertEqual(len(historique_bob), 1)
self.assertIn("Retrait", historique_alice[0])
self.assertIn("Dépôt", historique_bob[0])
# Tests d'historique
def test_historique_vide(self):
"""Test qu'un nouveau compte a un historique vide."""
compte = CompteBancaire("Test", 100)
self.assertEqual(len(compte.obtenir_historique()), 0)
def test_historique_multiple(self):
"""Test d'un historique avec plusieurs transactions."""
self.compte.deposer(100)
self.compte.retirer(50)
self.compte.deposer(200)
historique = self.compte.obtenir_historique()
self.assertEqual(len(historique), 3)
def test_historique_copie(self):
"""Test que obtenir_historique retourne une copie."""
historique = self.compte.obtenir_historique()
historique.append("Fausse transaction")
# L'historique du compte ne doit pas être modifié
self.assertEqual(len(self.compte.obtenir_historique()), 0)
if __name__ == '__main__':
unittest.main(verbosity=2)
Exécution avec verbosité :
python test_banking.py
Résultat :
test_creation_compte_sans_solde (__main__.TestCompteBancaire) ... ok
test_creation_compte_valide (__main__.TestCompteBancaire) ... ok
test_creation_solde_negatif (__main__.TestCompteBancaire) ... ok
test_depot_historique (__main__.TestCompteBancaire) ... ok
test_depot_negatif (__main__.TestCompteBancaire) ... ok
test_depot_valide (__main__.TestCompteBancaire) ... ok
test_depot_zero (__main__.TestCompteBancaire) ... ok
...
----------------------------------------------------------------------
Ran 15 tests in 0.003s
OK
Organiser ses tests
Structure de projet recommandée
mon_projet/
├── src/
│ ├── __init__.py
│ ├── banking.py
│ ├── calculatrice.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── test_banking.py
│ ├── test_calculatrice.py
│ └── test_utils.py
├── requirements.txt
└── README.md
Exécuter tous les tests
# Découverte automatique des tests
python -m unittest discover tests
# Avec verbosité
python -m unittest discover tests -v
# Exécuter un fichier spécifique
python -m unittest tests.test_banking
# Exécuter une classe spécifique
python -m unittest tests.test_banking.TestCompteBancaire
# Exécuter un test spécifique
python -m unittest tests.test_banking.TestCompteBancaire.test_depot_valide
Test-Driven Development (TDD)
Le cycle Red-Green-Refactor
Le TDD suit un cycle en trois étapes :
-
🔴 Red : Écrire un test qui échoue
-
🟢 Green : Écrire le code minimal pour passer le test
-
🔵 Refactor : Améliorer le code sans casser les tests
Exemple : Développer une fonction avec TDD
# Étape 1 : 🔴 RED - Écrire le test d'abord
class TestValidateurEmail(unittest.TestCase):
def test_email_valide(self):
"""Test qu'un email valide est accepté."""
self.assertTrue(valider_email("alice@exemple.com"))
def test_email_sans_arobase(self):
"""Test qu'un email sans @ est rejeté."""
self.assertFalse(valider_email("alice.exemple.com"))
def test_email_sans_domaine(self):
"""Test qu'un email sans domaine est rejeté."""
self.assertFalse(valider_email("alice@"))
def test_email_sans_extension(self):
"""Test qu'un email sans extension est rejeté."""
self.assertFalse(valider_email("alice@exemple"))
# Étape 2 : 🟢 GREEN - Implémenter pour passer les tests
def valider_email(email):
"""
Valide une adresse email.
Returns:
bool: True si l'email est valide, False sinon
"""
if '@' not in email:
return False
parties = email.split('@')
if len(parties) != 2:
return False
local, domaine = parties
if not local or not domaine:
return False
if '.' not in domaine:
return False
return True
# Étape 3 : 🔵 REFACTOR - Améliorer avec regex
import re
def valider_email(email):
"""Valide une adresse email."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
# Les tests passent toujours !
Mocking : simuler des dépendances
Pourquoi mocker ?
Parfois, vous devez tester du code qui dépend de ressources externes :
-
Bases de données
-
APIs web
-
Système de fichiers
-
Services tiers
Les mocks permettent de simuler ces dépendances.
Utiliser unittest.mock
from unittest.mock import Mock, patch, MagicMock
import unittest
# Code à tester
import requests
def obtenir_utilisateur(user_id):
"""Récupère un utilisateur depuis une API."""
response = requests.get(f"https://api.exemple.com/users/{user_id}")
if response.status_code == 200:
return response.json()
return None
# Tests avec mock
class TestObtenirUtilisateur(unittest.TestCase):
@patch('requests.get')
def test_utilisateur_existe(self, mock_get):
"""Test quand l'utilisateur existe."""
# Configurer le mock
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {'id': 1, 'nom': 'Alice'}
mock_get.return_value = mock_response
# Exécuter
resultat = obtenir_utilisateur(1)
# Vérifier
self.assertEqual(resultat['nom'], 'Alice')
mock_get.assert_called_once_with("https://api.exemple.com/users/1")
@patch('requests.get')
def test_utilisateur_inexistant(self, mock_get):
"""Test quand l'utilisateur n'existe pas."""
mock_response = Mock()
mock_response.status_code = 404
mock_get.return_value = mock_response
resultat = obtenir_utilisateur(999)
self.assertIsNone(resultat)
Mocker des méthodes de classe
class ServiceEmail:
"""Service d'envoi d'emails."""
def envoyer(self, destinataire, message):
"""Envoie un email (appelle un service externe)."""
# Code complexe d'envoi d'email
pass
class GestionnaireUtilisateurs:
"""Gère les utilisateurs."""
def __init__(self, service_email):
self.service_email = service_email
self.utilisateurs = []
def inscrire_utilisateur(self, nom, email):
"""Inscrit un utilisateur et envoie un email de bienvenue."""
utilisateur = {'nom': nom, 'email': email}
self.utilisateurs.append(utilisateur)
# Envoyer email de bienvenue
self.service_email.envoyer(
email,
f"Bienvenue {nom} !"
)
return utilisateur
# Tests
class TestGestionnaireUtilisateurs(unittest.TestCase):
def setUp(self):
# Créer un mock du service email
self.mock_service_email = Mock(spec=ServiceEmail)
self.gestionnaire = GestionnaireUtilisateurs(self.mock_service_email)
def test_inscrire_utilisateur(self):
"""Test l'inscription d'un utilisateur."""
utilisateur = self.gestionnaire.inscrire_utilisateur("Alice", "alice@mail.com")
# Vérifier que l'utilisateur est ajouté
self.assertEqual(len(self.gestionnaire.utilisateurs), 1)
self.assertEqual(utilisateur['nom'], "Alice")
# Vérifier que l'email a été envoyé
self.mock_service_email.envoyer.assert_called_once_with(
"alice@mail.com",
"Bienvenue Alice !"
)
Mocker des fichiers
from unittest.mock import mock_open, patch
def lire_configuration(fichier):
"""Lit un fichier de configuration."""
with open(fichier, 'r') as f:
contenu = f.read()
return contenu.strip()
class TestLireConfiguration(unittest.TestCase):
@patch('builtins.open', new_callable=mock_open, read_data='DEBUG=True\n')
def test_lire_configuration(self, mock_file):
"""Test de lecture de fichier."""
resultat = lire_configuration('config.txt')
self.assertEqual(resultat, 'DEBUG=True')
mock_file.assert_called_once_with('config.txt', 'r')
Pytest : une alternative moderne
Installation
pip install pytest
Avantages de pytest
-
Syntaxe plus simple
-
Meilleurs messages d'erreur
-
Fixtures puissantes
-
Paramétrage facile
-
Plugins riches
Exemple basique avec pytest
# test_calculatrice_pytest.py
from calculatrice import additionner, soustraire, diviser
import pytest
def test_additionner():
"""Test de l'addition."""
assert additionner(2, 3) == 5
assert additionner(-1, 1) == 0
def test_soustraire():
"""Test de la soustraction."""
assert soustraire(5, 3) == 2
assert soustraire(0, 5) == -5
def test_diviser():
"""Test de la division."""
assert diviser(10, 2) == 5
def test_diviser_par_zero():
"""Test de la division par zéro."""
with pytest.raises(ValueError):
diviser(10, 0)
Exécution :
pytest test_calculatrice_pytest.py -v
Fixtures pytest
import pytest
@pytest.fixture
def compte():
"""Fixture qui fournit un compte bancaire."""
return CompteBancaire("Alice", 1000)
@pytest.fixture
def deux_comptes():
"""Fixture qui fournit deux comptes."""
compte1 = CompteBancaire("Alice", 1000)
compte2 = CompteBancaire("Bob", 500)
return compte1, compte2
# Utilisation
def test_depot(compte):
"""Test de dépôt avec fixture."""
compte.deposer(500)
assert compte.solde == 1500
def test_transfert(deux_comptes):
"""Test de transfert avec fixture."""
alice, bob = deux_comptes
alice.transferer(bob, 200)
assert alice.solde == 800
assert bob.solde == 700
Paramétrage de tests
@pytest.mark.parametrize("a,b,attendu", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
(100, 200, 300),
(-5, -3, -8),
])
def test_additionner_parametres(a, b, attendu):
"""Test paramétré de l'addition."""
assert additionner(a, b) == attendu
# Pytest exécutera 5 tests automatiquement !
Couverture de code
Installation
pip install coverage
Utilisation
# Exécuter les tests avec couverture
coverage run -m unittest discover tests
# Générer le rapport
coverage report
# Générer un rapport HTML
coverage html
# Ouvrir htmlcov/index.html dans un navigateur
Exemple de rapport :
Name Stmts Miss Cover
---------------------------------------
banking.py 45 2 96%
calculatrice.py 12 0 100%
---------------------------------------
TOTAL 57 2 96%
Objectifs de couverture
-
80-90% : bon objectif pour la plupart des projets
-
100% : souvent inutile et coûteux
-
< 70% : probablement insuffisant
Important : La couverture mesure ce qui est exécuté, pas ce qui est bien testé !
Bonnes pratiques
1. Un test, une assertion (idéalement)
# ❌ Plusieurs assertions non liées
def test_compte_mauvais(self):
compte = CompteBancaire("Alice", 1000)
self.assertEqual(compte.titulaire, "Alice") # Test du nom
self.assertEqual(compte.solde, 1000) # Test du solde
compte.deposer(500)
self.assertEqual(compte.solde, 1500) # Test du dépôt
# ✅ Tests séparés et focalisés
def test_creation_titulaire(self):
compte = CompteBancaire("Alice", 1000)
self.assertEqual(compte.titulaire, "Alice")
def test_creation_solde(self):
compte = CompteBancaire("Alice", 1000)
self.assertEqual(compte.solde, 1000)
def test_depot(self):
compte = CompteBancaire("Alice", 1000)
compte.deposer(500)
self.assertEqual(compte.solde, 1500)
2. Tests indépendants
# ❌ Tests dépendants (l'ordre compte)
class TestMauvais(unittest.TestCase):
compte = None
def test_1_creation(self):
self.compte = CompteBancaire("Alice", 1000)
def test_2_depot(self):
self.compte.deposer(500) # Dépend de test_1 !
# ✅ Tests indépendants
class TestBon(unittest.TestCase):
def setUp(self):
self.compte = CompteBancaire("Alice", 1000)
def test_creation(self):
self.assertEqual(self.compte.solde, 1000)
def test_depot(self):
self.compte.deposer(500)
self.assertEqual(self.compte.solde, 1500)
3. Noms descriptifs
# ❌ Noms peu clairs
def test_1(self):
pass
def test_compte(self):
pass
# ✅ Noms descriptifs
def test_retrait_avec_solde_insuffisant_leve_value_error(self):
pass
def test_depot_montant_negatif_leve_value_error(self):
pass
def test_transfert_met_a_jour_les_deux_comptes(self):
pass
4. Pattern AAA (Arrange, Act, Assert)
def test_transfert(self):
# Arrange - Préparer
compte_alice = CompteBancaire("Alice", 1000)
compte_bob = CompteBancaire("Bob", 500)
# Act - Exécuter
compte_alice.transferer(compte_bob, 200)
# Assert - Vérifier
self.assertEqual(compte_alice.solde, 800)
self.assertEqual(compte_bob.solde, 700)
5. Tester les cas limites
class TestCasLimites(unittest.TestCase):
def test_liste_vide(self):
"""Test avec liste vide."""
self.assertEqual(somme([]), 0)
def test_un_element(self):
"""Test avec un seul élément."""
self.assertEqual(somme([5]), 5)
def test_valeurs_negatives(self):
"""Test avec valeurs négatives."""
self.assertEqual(somme([-1, -2, -3]), -6)
def test_grands_nombres(self):
"""Test avec très grands nombres."""
self.assertEqual(somme([10**10, 10**10]), 2 * 10**10)
6. Ne pas tester les détails d'implémentation
# ❌ Test trop couplé à l'implémentation
def test_mauvais(self):
compte = CompteBancaire("Alice", 1000)
compte.deposer(100)
# Ne testez pas les détails internes
self.assertEqual(compte._solde_interne, 1100) # Mauvais !
# ✅ Test du comportement public
def test_bon(self):
compte = CompteBancaire("Alice", 1000)
compte.deposer(100)
# Testez l'interface publique
self.assertEqual(compte.solde, 1100) # Bon !
Conclusion
Les tests unitaires, ce n’est pas du “nice to have”, c’est ce qui te permet de faire évoluer ton code sans sueurs froides à chaque commit. Ils t’aident à détecter les bugs tôt, documenter ton code au passage et garder une base propre même après plusieurs refactors, tout en te donnant la confiance de shipper plus souvent sans peur de tout casser.

Laisser un commentaire