# Fichier: python_cheats/cheatsheets/unittest.txt
# Cheatsheet unittest - Guide Complet et Détaillé


[OK] INTRODUCTION À UNITTEST

# unittest est le framework de tests unitaires intégré à Python
# Basé sur JUnit (Java), il suit le pattern xUnit
# Disponible depuis Python 2.1+, aucune installation requise

# Concepts clés:
# - Test Case: Classe contenant les tests individuels
# - Test Suite: Collection de tests à exécuter ensemble
# - Test Runner: Orchestre l'exécution des tests
# - Test Fixture: Setup/teardown pour préparer/nettoyer l'environnement
# - Assertions: Vérifications des résultats attendus


[OK] STRUCTURE DE BASE

# === Test minimal ===
import unittest

class TestBasique(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

if __name__ == '__main__':
    unittest.main()

# Exécution:
# python test_basique.py
# python -m unittest test_basique.py
# python -m unittest test_basique.TestBasique
# python -m unittest test_basique.TestBasique.test_addition


# === Anatomie d'une classe de test ===
import unittest

class TestMaFonction(unittest.TestCase):
    """Documentation de la classe de test"""
    
    # setUp: Exécuté AVANT chaque méthode de test
    def setUp(self):
        """Préparation avant chaque test"""
        self.data = [1, 2, 3]
        self.temp_file = "test.txt"
    
    # tearDown: Exécuté APRÈS chaque méthode de test
    def tearDown(self):
        """Nettoyage après chaque test"""
        if os.path.exists(self.temp_file):
            os.remove(self.temp_file)
    
    # Les méthodes de test DOIVENT commencer par "test_"
    def test_longueur_liste(self):
        """Test de la longueur"""
        self.assertEqual(len(self.data), 3)
    
    def test_premier_element(self):
        """Test du premier élément"""
        self.assertEqual(self.data[0], 1)
    
    # Les méthodes ne commençant pas par "test_" ne sont pas exécutées
    def helper_fonction(self):
        """Fonction auxiliaire, non exécutée comme test"""
        return self.data[0] + self.data[-1]

if __name__ == '__main__':
    unittest.main()


[OK] MÉTHODES DE FIXTURE (SETUP/TEARDOWN)

# === Niveau instance (setUp/tearDown) ===
# Exécuté avant/après CHAQUE test
class TestFixtures(unittest.TestCase):
    
    def setUp(self):
        """Appelé avant chaque test"""
        print("Setup avant test")
        self.connexion = connect_database()
    
    def tearDown(self):
        """Appelé après chaque test"""
        print("Teardown après test")
        self.connexion.close()
    
    def test_un(self):
        print("Exécution test_un")
    
    def test_deux(self):
        print("Exécution test_deux")

# Ordre d'exécution:
# setUp -> test_un -> tearDown
# setUp -> test_deux -> tearDown


# === Niveau classe (setUpClass/tearDownClass) ===
# Exécuté UNE SEULE FOIS pour toute la classe
class TestFixturesClasse(unittest.TestCase):
    
    @classmethod
    def setUpClass(cls):
        """Appelé UNE fois avant tous les tests de la classe"""
        print("Setup classe - exécuté une fois")
        cls.connexion_partagee = connect_database()
        cls.donnees_lourdes = load_big_dataset()
    
    @classmethod
    def tearDownClass(cls):
        """Appelé UNE fois après tous les tests de la classe"""
        print("Teardown classe - exécuté une fois")
        cls.connexion_partagee.close()
    
    def setUp(self):
        """Toujours exécuté avant chaque test"""
        self.temp_data = []
    
    def test_un(self):
        # Accès aux ressources de classe
        print(f"Connexion: {self.connexion_partagee}")
    
    def test_deux(self):
        print(f"Données: {self.donnees_lourdes}")

# Ordre d'exécution:
# setUpClass
# setUp -> test_un -> tearDown
# setUp -> test_deux -> tearDown
# tearDownClass


# === Niveau module (setUpModule/tearDownModule) ===
# Exécuté UNE fois pour tout le fichier de test

def setUpModule():
    """Appelé UNE fois avant tous les tests du module"""
    print("Setup module")
    global connexion_globale
    connexion_globale = connect_database()

def tearDownModule():
    """Appelé UNE fois après tous les tests du module"""
    print("Teardown module")
    connexion_globale.close()

class TestA(unittest.TestCase):
    def test_a1(self):
        pass

class TestB(unittest.TestCase):
    def test_b1(self):
        pass

# Ordre d'exécution:
# setUpModule
# TestA.setUpClass -> tests de TestA -> TestA.tearDownClass
# TestB.setUpClass -> tests de TestB -> TestB.tearDownClass
# tearDownModule


[OK] ASSERTIONS - VÉRIFICATIONS DE BASE

# === Égalité et comparaison ===

# assertEqual(a, b): Vérifie a == b
self.assertEqual(2 + 2, 4)
self.assertEqual("hello", "hello")
self.assertEqual([1, 2], [1, 2])
self.assertEqual({'a': 1}, {'a': 1})

# assertNotEqual(a, b): Vérifie a != b
self.assertNotEqual(2 + 2, 5)
self.assertNotEqual("hello", "world")

# assertTrue(x): Vérifie que x est True
self.assertTrue(1 < 2)
self.assertTrue(bool([1, 2, 3]))
self.assertTrue("text".startswith("t"))

# assertFalse(x): Vérifie que x est False
self.assertFalse(1 > 2)
self.assertFalse(bool([]))
self.assertFalse("text".startswith("x"))

# assertIs(a, b): Vérifie a is b (même objet en mémoire)
a = [1, 2, 3]
b = a
self.assertIs(a, b)
self.assertIs(None, None)

# assertIsNot(a, b): Vérifie a is not b
a = [1, 2, 3]
b = [1, 2, 3]
self.assertIsNot(a, b)  # Même contenu mais objets différents

# assertIsNone(x): Vérifie x is None
self.assertIsNone(None)
result = fonction_retournant_none()
self.assertIsNone(result)

# assertIsNotNone(x): Vérifie x is not None
self.assertIsNotNone("texte")
self.assertIsNotNone([])


# === Comparaisons numériques ===

# assertGreater(a, b): a > b
self.assertGreater(5, 3)
self.assertGreater(10.5, 10.4)

# assertGreaterEqual(a, b): a >= b
self.assertGreaterEqual(5, 5)
self.assertGreaterEqual(5, 3)

# assertLess(a, b): a < b
self.assertLess(3, 5)
self.assertLess(-1, 0)

# assertLessEqual(a, b): a <= b
self.assertLessEqual(3, 3)
self.assertLessEqual(3, 5)


# === Vérifications de type et instance ===

# assertIsInstance(obj, classe): Vérifie isinstance(obj, classe)
self.assertIsInstance(5, int)
self.assertIsInstance("text", str)
self.assertIsInstance([1, 2], list)
self.assertIsInstance({'a': 1}, dict)
self.assertIsInstance(5, (int, float))  # Tuple de types

# assertNotIsInstance(obj, classe): Vérifie not isinstance(obj, classe)
self.assertNotIsInstance("5", int)
self.assertNotIsInstance([], dict)


[OK] ASSERTIONS - CONTENEURS ET SÉQUENCES

# === Appartenance ===

# assertIn(membre, conteneur): Vérifie membre in conteneur
self.assertIn(3, [1, 2, 3, 4])
self.assertIn('a', "banana")
self.assertIn('key', {'key': 'value'})
self.assertIn(2, {1, 2, 3})

# assertNotIn(membre, conteneur): Vérifie membre not in conteneur
self.assertNotIn(5, [1, 2, 3])
self.assertNotIn('z', "banana")


# === Séquences ===

# assertSequenceEqual(seq1, seq2): Compare deux séquences
self.assertSequenceEqual([1, 2, 3], [1, 2, 3])
self.assertSequenceEqual("abc", "abc")
# Affiche les différences en cas d'échec

# assertListEqual(list1, list2): Spécifique aux listes
self.assertListEqual([1, 2, 3], [1, 2, 3])
# Meilleurs messages d'erreur que assertEqual

# assertTupleEqual(tuple1, tuple2): Spécifique aux tuples
self.assertTupleEqual((1, 2, 3), (1, 2, 3))

# assertSetEqual(set1, set2): Spécifique aux sets
self.assertSetEqual({1, 2, 3}, {3, 2, 1})  # Ordre n'a pas d'importance

# assertDictEqual(dict1, dict2): Spécifique aux dictionnaires
self.assertDictEqual({'a': 1, 'b': 2}, {'b': 2, 'a': 1})
# Affiche clairement les différences entre dictionnaires


# === Sous-ensembles ===

# assertCountEqual(seq1, seq2): Compare sans tenir compte de l'ordre
# Anciennement assertItemsEqual en Python 2
self.assertCountEqual([1, 2, 3], [3, 2, 1])
self.assertCountEqual([1, 1, 2], [2, 1, 1])
self.assertCountEqual("abc", "bca")


[OK] ASSERTIONS - CHAÎNES DE CARACTÈRES

# assertRegex(text, regex): Vérifie que regex correspond
import re
self.assertRegex("hello123", r"hello\d+")
self.assertRegex("user@example.com", r".+@.+\..+")
self.assertRegex("2024-01-15", r"\d{4}-\d{2}-\d{2}")

# assertNotRegex(text, regex): Vérifie que regex ne correspond pas
self.assertNotRegex("hello", r"\d+")
self.assertNotRegex("abc", r"[0-9]")

# assertMultiLineEqual(first, second): Compare des chaînes multilignes
text1 = """ligne 1
ligne 2
ligne 3"""
text2 = """ligne 1
ligne 2
ligne 3"""
self.assertMultiLineEqual(text1, text2)
# Affiche les différences ligne par ligne en cas d'échec


[OK] ASSERTIONS - EXCEPTIONS

# === assertRaises ===

# Forme 1: Context manager (RECOMMANDÉE)
def diviser(a, b):
    if b == 0:
        raise ValueError("Division par zéro")
    return a / b

class TestExceptions(unittest.TestCase):
    
    def test_division_par_zero(self):
        # Vérifie qu'une exception ValueError est levée
        with self.assertRaises(ValueError):
            diviser(10, 0)
    
    def test_exception_avec_message(self):
        # Capture l'exception pour vérifier son message
        with self.assertRaises(ValueError) as context:
            diviser(10, 0)
        
        # Vérifier le message d'erreur
        self.assertEqual(str(context.exception), "Division par zéro")
        self.assertIn("zéro", str(context.exception))
    
    def test_type_erreur(self):
        with self.assertRaises(TypeError):
            diviser("10", 5)

# Forme 2: Comme méthode (ancienne méthode)
def test_exception_methode(self):
    self.assertRaises(ValueError, diviser, 10, 0)
    # équivalent à: assertRaises(exception, fonction, *args, **kwargs)


# === assertRaisesRegex ===

def valider_email(email):
    if '@' not in email:
        raise ValueError(f"Email invalide: {email}")
    return True

class TestExceptionsRegex(unittest.TestCase):
    
    def test_email_invalide(self):
        # Vérifie l'exception ET le message avec regex
        with self.assertRaisesRegex(ValueError, r"Email invalide: .+"):
            valider_email("mauvais-email")
        
        with self.assertRaisesRegex(ValueError, "invalide"):
            valider_email("test")


# === assertWarns (Python 3.2+) ===

import warnings

def fonction_deprecatee():
    warnings.warn("Cette fonction est obsolète", DeprecationWarning)
    return "résultat"

class TestWarnings(unittest.TestCase):
    
    def test_warning(self):
        # Vérifie qu'un warning est émis
        with self.assertWarns(DeprecationWarning):
            fonction_deprecatee()
    
    def test_warning_avec_message(self):
        with self.assertWarns(DeprecationWarning) as w:
            fonction_deprecatee()
        
        self.assertIn("obsolète", str(w.warning))

# assertWarnsRegex: Comme assertWarns mais avec regex
def test_warning_regex(self):
    with self.assertWarnsRegex(DeprecationWarning, "obsolète"):
        fonction_deprecatee()


[OK] ASSERTIONS - NOMBRES FLOTTANTS

# === assertAlmostEqual / assertNotAlmostEqual ===

# Problème avec les floats:
0.1 + 0.2 == 0.3  # False! (0.30000000000000004)

class TestFlottants(unittest.TestCase):
    
    def test_presque_egal(self):
        # Par défaut: 7 décimales de précision
        self.assertAlmostEqual(0.1 + 0.2, 0.3)
        self.assertAlmostEqual(1.0000001, 1.0000002)
    
    def test_places_decimales(self):
        # Spécifier le nombre de décimales
        self.assertAlmostEqual(0.12345, 0.12346, places=4)
        # Vérifie: round(a-b, 4) == 0
        
        self.assertAlmostEqual(1.5, 1.501, places=2)  # OK
        # self.assertAlmostEqual(1.5, 1.501, places=3)  # FAIL
    
    def test_delta(self):
        # Utiliser un delta absolu au lieu de places
        self.assertAlmostEqual(1.0, 1.1, delta=0.2)
        # Vérifie: abs(a-b) <= delta
        
        self.assertAlmostEqual(100, 101, delta=2)  # OK
    
    def test_pas_presque_egal(self):
        self.assertNotAlmostEqual(1.0, 2.0)
        self.assertNotAlmostEqual(0.1, 0.2, places=1)


# === Exemple pratique ===

import math

class TestCalculsScientifiques(unittest.TestCase):
    
    def test_racine_carree(self):
        resultat = math.sqrt(2) ** 2
        # Ne PAS faire: self.assertEqual(resultat, 2.0)
        self.assertAlmostEqual(resultat, 2.0)
    
    def test_pi(self):
        pi_approx = 22 / 7
        self.assertAlmostEqual(pi_approx, math.pi, places=2)
    
    def test_calcul_financier(self):
        montant = 10.01
        tva = montant * 0.2
        total = montant + tva
        self.assertAlmostEqual(total, 12.012, places=2)


[OK] MESSAGES D'ERREUR PERSONNALISÉS

# Toutes les assertions acceptent un paramètre msg optionnel

class TestMessages(unittest.TestCase):
    
    def test_avec_message(self):
        valeur = calcul_complexe()
        self.assertEqual(
            valeur, 
            42, 
            msg=f"Le calcul a retourné {valeur} au lieu de 42"
        )
    
    def test_message_contexte(self):
        for i in range(10):
            with self.subTest(i=i):
                self.assertEqual(
                    fonction(i), 
                    i * 2,
                    msg=f"Échec pour i={i}"
                )
    
    def test_message_detaille(self):
        donnees = {'a': 1, 'b': 2}
        self.assertIn(
            'c', 
            donnees,
            msg=f"Clé 'c' manquante. Données disponibles: {list(donnees.keys())}"
        )


[OK] SUBTESTS - TESTS PARAMÉTRÉS

# Les subTests permettent de continuer l'exécution même si un test échoue

class TestSubTests(unittest.TestCase):
    
    # === Sans subTest (s'arrête au premier échec) ===
    def test_sans_subtest(self):
        for i in [0, 1, 2, 3, 4]:
            self.assertEqual(i % 2, 0)  # S'arrête à i=1
    
    # === Avec subTest (continue après échec) ===
    def test_avec_subtest(self):
        for i in [0, 1, 2, 3, 4]:
            with self.subTest(i=i):
                self.assertEqual(i % 2, 0)
        # Affiche tous les échecs (i=1, i=3)
    
    # === Paramètres nommés ===
    def test_parametres_multiples(self):
        test_cases = [
            (2, 3, 5),
            (5, 5, 10),
            (0, 0, 0),
            (10, -5, 5),
        ]
        
        for a, b, resultat_attendu in test_cases:
            with self.subTest(a=a, b=b):
                self.assertEqual(a + b, resultat_attendu)
    
    # === Avec dictionnaires ===
    def test_validation_email(self):
        emails = {
            'user@example.com': True,
            'invalid-email': False,
            'test@test.co.uk': True,
            '@example.com': False,
            'user@': False,
        }
        
        for email, valide in emails.items():
            with self.subTest(email=email):
                self.assertEqual(valider_email(email), valide)
    
    # === Avec descriptions ===
    def test_avec_descriptions(self):
        with self.subTest("Test des entiers positifs"):
            self.assertGreater(5, 0)
        
        with self.subTest("Test des entiers négatifs"):
            self.assertLess(-5, 0)
        
        with self.subTest("Test de zéro"):
            self.assertEqual(0, 0)


[OK] SKIP - SAUTER DES TESTS

# === skip: Sauter un test ===

class TestSkip(unittest.TestCase):
    
    @unittest.skip("Fonctionnalité pas encore implémentée")
    def test_futur(self):
        self.assertEqual(fonctionnalite_future(), "résultat")
    
    @unittest.skip("Test temporairement désactivé")
    def test_problematique(self):
        pass

# === skipIf: Sauter selon une condition ===

import sys
import platform

class TestSkipIf(unittest.TestCase):
    
    @unittest.skipIf(sys.version_info < (3, 10), "Nécessite Python 3.10+")
    def test_nouvelle_syntaxe(self):
        # Test utilisant match/case de Python 3.10
        pass
    
    @unittest.skipIf(platform.system() == "Windows", "Ne fonctionne pas sous Windows")
    def test_unix_seulement(self):
        import pwd  # Module Unix seulement
        pass
    
    @unittest.skipIf(not has_network(), "Pas de connexion réseau")
    def test_api_externe(self):
        response = requests.get("https://api.example.com")
        self.assertEqual(response.status_code, 200)

# === skipUnless: Sauter sauf si condition ===

class TestSkipUnless(unittest.TestCase):
    
    @unittest.skipUnless(sys.platform.startswith("linux"), "Linux seulement")
    def test_linux_specifique(self):
        pass
    
    @unittest.skipUnless(has_database(), "Base de données non disponible")
    def test_database(self):
        pass

# === Sauter toute une classe ===

@unittest.skip("Classe entière désactivée")
class TestClasseSkip(unittest.TestCase):
    def test_un(self):
        pass
    
    def test_deux(self):
        pass

# === skipTest: Sauter dynamiquement ===

class TestSkipDynamique(unittest.TestCase):
    
    def test_conditionnel(self):
        if not ressource_disponible():
            self.skipTest("Ressource non disponible")
        
        # Test continue seulement si ressource disponible
        self.assertTrue(utiliser_ressource())
    
    def test_configuration(self):
        config = charger_config()
        if 'api_key' not in config:
            self.skipTest("API key manquante dans configuration")
        
        # Test API


[OK] EXPECTED FAILURES - ÉCHECS ATTENDUS

# === expectedFailure: Test censé échouer ===

class TestExpectedFailure(unittest.TestCase):
    
    @unittest.expectedFailure
    def test_bug_connu(self):
        """Test d'un bug connu pas encore corrigé"""
        self.assertEqual(fonction_buguee(), "résultat attendu")
        # Si échoue: marqué comme "expected failure" (OK)
        # Si réussit: marqué comme "unexpected success" (ALERTE)
    
    @unittest.expectedFailure
    def test_comportement_futur(self):
        """Test du comportement futur après refactoring"""
        self.assertTrue(nouvelle_implementation())


# Cas d'usage:
# 1. Bug connu en attente de correction
# 2. Fonctionnalité en développement
# 3. Tests pour comportement futur
# 4. Incompatibilités temporaires


[OK] EXÉCUTION DES TESTS

# === Ligne de commande ===

# Exécuter un fichier de test
python test_module.py
python -m unittest test_module.py

# Exécuter un module
python -m unittest test_module

# Exécuter une classe spécifique
python -m unittest test_module.TestMaClasse

# Exécuter une méthode spécifique
python -m unittest test_module.TestMaClasse.test_methode

# Découverte automatique (tous les tests du projet)
python -m unittest discover
python -m unittest discover -s tests -p "test_*.py"
# -s: répertoire de départ (défaut: .)
# -p: pattern des fichiers (défaut: test*.py)
# -t: répertoire racine du projet

# Exécution verbale
python -m unittest -v test_module.py
python -m unittest discover -v

# Arrêter au premier échec
python -m unittest --failfast
python -m unittest -f

# Afficher variables locales en cas d'échec
python -m unittest --locals
python -m unittest -l

# Buffer output (capturer print)
python -m unittest --buffer
python -m unittest -b

# Catch (capturer Ctrl+C proprement)
python -m unittest --catch
python -m unittest -c


# === Dans le code ===

# Méthode 1: unittest.main()
if __name__ == '__main__':
    unittest.main()

# Méthode 2: unittest.main() avec options
if __name__ == '__main__':
    unittest.main(verbosity=2, failfast=True)

# Méthode 3: Test suite manuel
if __name__ == '__main__':
    loader = unittest.TestLoader()
    suite = loader.loadTestsFromTestCase(TestMaClasse)
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite)

# Méthode 4: Combiner plusieurs suites
if __name__ == '__main__':
    suite = unittest.TestSuite()
    suite.addTest(TestA('test_methode1'))
    suite.addTest(TestB('test_methode2'))
    runner = unittest.TextTestRunner()
    runner.run(suite)


[OK] TEST DISCOVERY - DÉCOUVERTE AUTOMATIQUE

# Structure de projet pour discovery
myproject/
├── src/
│   └── mypackage/
│       ├── __init__.py
│       └── module.py
├── tests/
│   ├── __init__.py
│   ├── test_module.py
│   ├── test_autre.py
│   └── integration/
│       ├── __init__.py
│       └── test_integration.py
└── setup.py

# Depuis la racine du projet:
python -m unittest discover
python -m unittest discover -s tests
python -m unittest discover -s tests -p "test_*.py"

# Découvrir seulement tests d'intégration
python -m unittest discover -s tests/integration

# Découvrir avec pattern personnalisé
python -m unittest discover -p "*_test.py"


# === Conventions de nommage ===

# Fichiers: Doivent matcher le pattern (défaut: test*.py)
# [OK] test_module.py
# [OK] test_user.py
# [OK] tests.py
# [X] module_test.py (ne match pas pattern par défaut)

# Classes: Doivent hériter de unittest.TestCase
# [OK] class TestUser(unittest.TestCase)
# [OK] class UserTestCase(unittest.TestCase)

# Méthodes: Doivent commencer par "test"
# [OK] def test_creation(self)
# [OK] def test_suppression(self)
# [X] def verify_user(self)  # Ne sera pas exécuté


[OK] TEST RUNNERS PERSONNALISÉS

# === TextTestRunner (par défaut) ===

runner = unittest.TextTestRunner(
    verbosity=2,        # 0=silencieux, 1=normal, 2=verbeux
    stream=sys.stdout,  # Où afficher les résultats
    descriptions=True,  # Afficher docstrings
    failfast=False,     # Arrêter au premier échec
    buffer=False,       # Capturer stdout/stderr
    resultclass=None,   # Classe résultat personnalisée
)

suite = unittest.TestLoader().loadTestsFromTestCase(TestMaClasse)
result = runner.run(suite)

# Analyser résultats
print(f"Tests exécutés: {result.testsRun}")
print(f"Échecs: {len(result.failures)}")
print(f"Erreurs: {len(result.errors)}")
print(f"Skipped: {len(result.skipped)}")
print(f"Succès: {result.wasSuccessful()}")


# === Runner personnalisé ===

class CustomTestRunner(unittest.TextTestRunner):
    def run(self, test):
        print("Début des tests...")
        result = super().run(test)
        print(f"\n{'='*70}")
        print(f"Résumé: {result.testsRun} tests")
        if result.wasSuccessful():
            print("[OK] Tous les tests ont réussi!")
        else:
            print(f"[X] {len(result.failures)} échec(s)")
            print(f"[X] {len(result.errors)} erreur(s)")
        return result

runner = CustomTestRunner(verbosity=2)
suite = unittest.TestLoader().loadTestsFromModule(test_module)
runner.run(suite)


[OK] TEST SUITES - ORGANISER LES TESTS

# === Créer une suite simple ===

suite = unittest.TestSuite()
suite.addTest(TestMaClasse('test_methode1'))
suite.addTest(TestMaClasse('test_methode2'))
suite.addTest(TestAutreClasse('test_autre'))

runner = unittest.TextTestRunner()
runner.run(suite)


# === TestLoader pour charger automatiquement ===

loader = unittest.TestLoader()

# Charger depuis une classe
suite = loader.loadTestsFromTestCase(TestMaClasse)

# Charger depuis un module
import test_module
suite = loader.loadTestsFromModule(test_module)

# Charger depuis plusieurs classes
suite = unittest.TestSuite()
suite.addTests(loader.loadTestsFromTestCase(TestA))
suite.addTests(loader.loadTestsFromTestCase(TestB))
suite.addTests(loader.loadTestsFromTestCase(TestC))

# Charger avec des noms
suite = loader.loadTestsFromNames([
    'test_module.TestMaClasse.test_methode1',
    'test_module.TestMaClasse.test_methode2',
    'test_autre.TestAutre',
])


# === Suite hiérarchique ===

def suite_complete():
    """Créer une suite complète de tests"""
    loader = unittest.TestLoader()
    suite = unittest.TestSuite()
    
    # Tests unitaires
    suite.addTests(loader.loadTestsFromModule(test_models))
    suite.addTests(loader.loadTestsFromModule(test_views))
    suite.addTests(loader.loadTestsFromModule(test_controllers))
    
    # Tests d'intégration
    suite.addTests(loader.loadTestsFromModule(test_integration))
    
    return suite

if __name__ == '__main__':
    runner = unittest.TextTestRunner(verbosity=2)
    runner.run(suite_complete())


# === Suite avec ordre d'exécution ===

class OrderedTestSuite(unittest.TestSuite):
    """Suite qui exécute les tests dans un ordre spécifique"""
    
    def __init__(self):
        super().__init__()
        # Ajouter tests dans l'ordre désiré
        self.addTest(TestDatabase('test_connection'))
        self.addTest(TestDatabase('test_create_table'))
        self.addTest(TestDatabase('test_insert'))
        self.addTest(TestDatabase('test_query'))
        self.addTest(TestDatabase('test_cleanup'))


[OK] MOCK ET PATCH - SIMULATION D'OBJETS

# unittest.mock est inclus depuis Python 3.3+
from unittest.mock import Mock, MagicMock, patch, call

# === Mock basique ===

class TestMock(unittest.TestCase):
    
    def test_mock_simple(self):
        # Créer un mock
        mock_obj = Mock()
        
        # Configurer une valeur de retour
        mock_obj.methode.return_value = 42
        
        # Appeler et vérifier
        resultat = mock_obj.methode()
        self.assertEqual(resultat, 42)
        
        # Vérifier que la méthode a été appelée
        mock_obj.methode.assert_called_once()
    
    def test_mock_avec_attributs(self):
        mock_obj = Mock()
        mock_obj.nom = "Test"
        mock_obj.age = 25
        
        self.assertEqual(mock_obj.nom, "Test")
        self.assertEqual(mock_obj.age, 25)
    
    def test_mock_side_effect(self):
        # side_effect pour exceptions
        mock_obj = Mock()
        mock_obj.methode.side_effect = ValueError("Erreur test")
        
        with self.assertRaises(ValueError):
            mock_obj.methode()
        
        # side_effect pour valeurs multiples
        mock_obj.compteur.side_effect = [1, 2, 3]
        self.assertEqual(mock_obj.compteur(), 1)
        self.assertEqual(mock_obj.compteur(), 2)
        self.assertEqual(mock_obj.compteur(), 3)


# === MagicMock - Mock avec méthodes magiques ===

class TestMagicMock(unittest.TestCase):
    
    def test_magic_mock(self):
        mock_list = MagicMock()
        
        # Supporte __len__, __getitem__, etc.
        mock_list.__len__.return_value = 3
        mock_list.__getitem__.return_value = "valeur"
        
        self.assertEqual(len(mock_list), 3)
        self.assertEqual(mock_list[0], "valeur")
    
    def test_context_manager(self):
        # MagicMock supporte with
        mock_file = MagicMock()
        
        with mock_file as f:
            f.read.return_value = "contenu"
            self.assertEqual(f.read(), "contenu")


# === patch - Remplacer temporairement ===

# Fichier: my_module.py
def obtenir_utilisateur():
    # Appel API coûteux
    return requests.get('https://api.example.com/user').json()

def traiter_utilisateur():
    user = obtenir_utilisateur()
    return f"Bonjour {user['nom']}"


# Fichier: test_my_module.py
class TestPatch(unittest.TestCase):
    
    # Méthode 1: Décorateur
    @patch('my_module.obtenir_utilisateur')
    def test_avec_decorateur(self, mock_obtenir):
        # Configurer le mock
        mock_obtenir.return_value = {'nom': 'Alice', 'id': 1}
        
        # Tester
        resultat = traiter_utilisateur()
        self.assertEqual(resultat, "Bonjour Alice")
        mock_obtenir.assert_called_once()
    
    # Méthode 2: Context manager
    def test_avec_context_manager(self):
        with patch('my_module.obtenir_utilisateur') as mock_obtenir:
            mock_obtenir.return_value = {'nom': 'Bob', 'id': 2}
            
            resultat = traiter_utilisateur()
            self.assertEqual(resultat, "Bonjour Bob")
    
    # Méthode 3: Manuel (start/stop)
    def test_manuel(self):
        patcher = patch('my_module.obtenir_utilisateur')
        mock_obtenir = patcher.start()
        mock_obtenir.return_value = {'nom': 'Charlie', 'id': 3}
        
        try:
            resultat = traiter_utilisateur()
            self.assertEqual(resultat, "Bonjour Charlie")
        finally:
            patcher.stop()


# === Patch multiple ===

class TestPatchMultiple(unittest.TestCase):
    
    @patch('my_module.fonction_a')
    @patch('my_module.fonction_b')
    def test_deux_patches(self, mock_b, mock_a):
        # ATTENTION: ordre inversé dans les paramètres!
        mock_a.return_value = "A"
        mock_b.return_value = "B"
        
        # Tests...
    
    # Alternative: patch.multiple
    @patch.multiple('my_module',
                    fonction_a=Mock(return_value="A"),
                    fonction_b=Mock(return_value="B"))
    def test_patch_multiple(self):
        # Les fonctions sont maintenant mockées
        pass


# === Patch d'objets ===

class TestPatchObject(unittest.TestCase):
    
    def test_patch_object(self):
        # Patcher une méthode d'un objet
        obj = MaClasse()
        
        with patch.object(obj, 'methode', return_value=42):
            self.assertEqual(obj.methode(), 42)
    
    def test_patch_class_method(self):
        # Patcher une méthode de classe
        with patch.object(MaClasse, 'methode_classe', return_value="mocké"):
            self.assertEqual(MaClasse.methode_classe(), "mocké")


# === Patch d'attributs ===

class TestPatchDict(unittest.TestCase):
    
    def test_patch_dict(self):
        # Patcher un dictionnaire (ex: os.environ)
        import os
        
        with patch.dict(os.environ, {'API_KEY': 'test_key'}):
            self.assertEqual(os.environ['API_KEY'], 'test_key')
        
        # API_KEY n'existe plus après le with


# === Vérifications d'appels ===

class TestAssertions(unittest.TestCase):
    
    def test_verifications_appels(self):
        mock = Mock()
        
        # Appeler plusieurs fois
        mock.methode(1, 2, key='value')
        mock.methode(3, 4)
        mock.autre_methode()
        
        # assert_called(): au moins un appel
        mock.methode.assert_called()
        
        # assert_called_once(): exactement un appel
        mock.autre_methode.assert_called_once()
        
        # assert_called_with(): dernier appel avec args
        mock.methode.assert_called_with(3, 4)
        
        # assert_called_once_with(): un seul appel avec args
        mock.autre_methode.assert_called_once_with()
        
        # assert_any_call(): au moins un appel avec args
        mock.methode.assert_any_call(1, 2, key='value')
        
        # assert_has_calls(): vérifier plusieurs appels dans l'ordre
        mock.methode.assert_has_calls([
            call(1, 2, key='value'),
            call(3, 4)
        ])
        
        # assert_not_called(): jamais appelé
        mock_non_appele = Mock()
        mock_non_appele.assert_not_called()
        
        # call_count: nombre d'appels
        self.assertEqual(mock.methode.call_count, 2)
        
        # call_args: arguments du dernier appel
        self.assertEqual(mock.methode.call_args, call(3, 4))
        
        # call_args_list: liste de tous les appels
        self.assertEqual(len(mock.methode.call_args_list), 2)


# === Mock de classes ===

class TestMockClasse(unittest.TestCase):
    
    @patch('my_module.MaClasse')
    def test_mock_classe(self, MockClass):
        # Configurer l'instance mockée
        instance = MockClass.return_value
        instance.methode.return_value = "résultat"
        
        # Utiliser la classe (crée une instance mockée)
        obj = MaClasse()
        resultat = obj.methode()
        
        # Vérifications
        MockClass.assert_called_once()  # Constructeur appelé
        instance.methode.assert_called_once()
        self.assertEqual(resultat, "résultat")


# === Spec - Mock avec spécification ===

class MaClasse:
    def methode_existante(self):
        pass

class TestSpec(unittest.TestCase):
    
    def test_sans_spec(self):
        # Sans spec: peut appeler n'importe quoi
        mock = Mock()
        mock.methode_inexistante()  # OK (pas d'erreur)
    
    def test_avec_spec(self):
        # Avec spec: seulement méthodes existantes
        mock = Mock(spec=MaClasse)
        mock.methode_existante()  # OK
        
        # mock.methode_inexistante()  # AttributeError!
    
    def test_spec_set(self):
        # spec_set: interdit aussi de définir nouveaux attributs
        mock = Mock(spec_set=MaClasse)
        # mock.nouvel_attribut = "valeur"  # AttributeError!


[OK] TESTS D'INTÉGRATION ET FIXTURES AVANCÉES

# === Fixtures avec fichiers temporaires ===

import tempfile
import os

class TestAvecFichiers(unittest.TestCase):
    
    def setUp(self):
        # Créer un répertoire temporaire
        self.test_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.test_dir, 'test.txt')
        
        # Créer un fichier de test
        with open(self.test_file, 'w') as f:
            f.write("Contenu de test")
    
    def tearDown(self):
        # Nettoyer
        if os.path.exists(self.test_file):
            os.remove(self.test_file)
        if os.path.exists(self.test_dir):
            os.rmdir(self.test_dir)
    
    def test_lecture_fichier(self):
        with open(self.test_file, 'r') as f:
            contenu = f.read()
        self.assertEqual(contenu, "Contenu de test")


# === Fixtures avec base de données ===

import sqlite3

class TestDatabase(unittest.TestCase):
    
    @classmethod
    def setUpClass(cls):
        # Créer DB en mémoire (partagée par tous les tests)
        cls.connexion = sqlite3.connect(':memory:')
        cls.cursor = cls.connexion.cursor()
        
        # Créer tables
        cls.cursor.execute('''
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                nom TEXT NOT NULL,
                email TEXT UNIQUE
            )
        ''')
        cls.connexion.commit()
    
    @classmethod
    def tearDownClass(cls):
        # Fermer connexion
        cls.connexion.close()
    
    def setUp(self):
        # Nettoyer données avant chaque test
        self.cursor.execute('DELETE FROM users')
        self.connexion.commit()
    
    def test_insert_user(self):
        self.cursor.execute(
            'INSERT INTO users (nom, email) VALUES (?, ?)',
            ('Alice', 'alice@example.com')
        )
        self.connexion.commit()
        
        self.cursor.execute('SELECT * FROM users WHERE nom = ?', ('Alice',))
        user = self.cursor.fetchone()
        
        self.assertIsNotNone(user)
        self.assertEqual(user[1], 'Alice')
    
    def test_unique_email(self):
        self.cursor.execute(
            'INSERT INTO users (nom, email) VALUES (?, ?)',
            ('Bob', 'bob@example.com')
        )
        self.connexion.commit()
        
        # Tentative d'insertion doublon
        with self.assertRaises(sqlite3.IntegrityError):
            self.cursor.execute(
                'INSERT INTO users (nom, email) VALUES (?, ?)',
                ('Bob2', 'bob@example.com')
            )


# === Fixtures avec serveur de test ===

from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading

class TestServeur(unittest.TestCase):
    
    @classmethod
    def setUpClass(cls):
        # Démarrer serveur HTTP de test
        cls.server = HTTPServer(('localhost', 8888), SimpleHTTPRequestHandler)
        cls.server_thread = threading.Thread(target=cls.server.serve_forever)
        cls.server_thread.daemon = True
        cls.server_thread.start()
    
    @classmethod
    def tearDownClass(cls):
        # Arrêter serveur
        cls.server.shutdown()
    
    def test_requete_http(self):
        import urllib.request
        response = urllib.request.urlopen('http://localhost:8888/')
        self.assertEqual(response.status, 200)


[OK] TESTS ASYNCHRONES (Python 3.8+)

# unittest supporte les tests async/await

import asyncio

class TestAsync(unittest.IsolatedAsyncioTestCase):
    
    async def asyncSetUp(self):
        """setUp asynchrone"""
        self.connexion = await creer_connexion_async()
    
    async def asyncTearDown(self):
        """tearDown asynchrone"""
        await self.connexion.close()
    
    async def test_fonction_async(self):
        """Test d'une fonction asynchrone"""
        resultat = await fonction_async()
        self.assertEqual(resultat, "valeur attendue")
    
    async def test_plusieurs_await(self):
        """Test avec plusieurs await"""
        resultat1 = await fonction_async_1()
        resultat2 = await fonction_async_2()
        
        self.assertEqual(resultat1, "valeur1")
        self.assertEqual(resultat2, "valeur2")
    
    async def test_avec_timeout(self):
        """Test avec timeout"""
        with self.assertRaises(asyncio.TimeoutError):
            await asyncio.wait_for(fonction_lente(), timeout=1.0)


# === Mock de fonctions async ===

from unittest.mock import AsyncMock

class TestAsyncMock(unittest.IsolatedAsyncioTestCase):
    
    @patch('my_module.fonction_async')
    async def test_mock_async(self, mock_fonction):
        # Utiliser AsyncMock
        mock_fonction.return_value = "résultat mocké"
        
        resultat = await fonction_async()
        self.assertEqual(resultat, "résultat mocké")
    
    async def test_async_mock_direct(self):
        mock = AsyncMock(return_value="valeur")
        resultat = await mock()
        self.assertEqual(resultat, "valeur")


[OK] TESTS DE PERFORMANCE ET BENCHMARKING

# Mesurer le temps d'exécution

import time

class TestPerformance(unittest.TestCase):
    
    def test_temps_execution(self):
        """Test qu'une fonction s'exécute en moins de X secondes"""
        debut = time.time()
        
        fonction_a_tester()
        
        duree = time.time() - debut
        self.assertLess(duree, 1.0, "Fonction trop lente!")
    
    def test_comparaison_performance(self):
        """Comparer performance de deux implémentations"""
        # Mesurer ancienne version
        debut = time.time()
        for _ in range(1000):
            ancienne_version()
        duree_ancienne = time.time() - debut
        
        # Mesurer nouvelle version
        debut = time.time()
        for _ in range(1000):
            nouvelle_version()
        duree_nouvelle = time.time() - debut
        
        # Nouvelle version doit être plus rapide
        self.assertLess(duree_nouvelle, duree_ancienne)
    
    def test_complexite(self):
        """Test de complexité algorithmique"""
        import numpy as np
        
        tailles = [100, 200, 400, 800]
        temps = []
        
        for n in tailles:
            data = list(range(n))
            debut = time.time()
            algorithme(data)
            temps.append(time.time() - debut)
        
        # Vérifier complexité O(n) vs O(n²)
        # Pour O(n): doubler taille ~double temps
        # Pour O(n²): doubler taille ~quadruple temps
        ratio = temps[1] / temps[0]
        self.assertLess(ratio, 2.5, "Complexité pire que O(n)")


[OK] TESTS PARAMÉTRÉS AVANCÉS

# === Utiliser subTest avec @parametrize (style pytest) ===

def parametrize(test_cases):
    """Décorateur pour tests paramétrés"""
    def decorator(test_func):
        def wrapper(self):
            for params in test_cases:
                with self.subTest(**params):
                    test_func(self, **params)
        return wrapper
    return decorator

class TestParametre(unittest.TestCase):
    
    @parametrize([
        {'x': 1, 'y': 2, 'resultat': 3},
        {'x': 5, 'y': 5, 'resultat': 10},
        {'x': -1, 'y': 1, 'resultat': 0},
    ])
    def test_addition(self, x, y, resultat):
        self.assertEqual(x + y, resultat)


# === Data-driven tests ===

class TestDataDriven(unittest.TestCase):
    
    def test_validation_emails(self):
        """Tests à partir de données externes"""
        test_data = [
            ('user@example.com', True),
            ('invalid.email', False),
            ('test@test.co.uk', True),
            ('@example.com', False),
            ('user@.com', False),
            ('user@domain.', False),
        ]
        
        for email, attendu in test_data:
            with self.subTest(email=email):
                resultat = valider_email(email)
                self.assertEqual(resultat, attendu)
    
    def test_depuis_fichier_csv(self):
        """Charger cas de test depuis CSV"""
        import csv
        
        with open('test_cases.csv', 'r') as f:
            reader = csv.DictReader(f)
            for row in reader:
                with self.subTest(row=row):
                    entree = row['input']
                    attendu = row['expected']
                    resultat = fonction(entree)
                    self.assertEqual(resultat, attendu)


[OK] ASSERTIONS PERSONNALISÉES

# === Créer ses propres assertions ===

class CustomAssertions:
    """Mixin avec assertions personnalisées"""
    
    def assertIsEmpty(self, conteneur, msg=None):
        """Vérifie qu'un conteneur est vide"""
        if len(conteneur) != 0:
            msg = msg or f"{conteneur!r} n'est pas vide"
            raise self.failureException(msg)
    
    def assertIsNotEmpty(self, conteneur, msg=None):
        """Vérifie qu'un conteneur n'est pas vide"""
        if len(conteneur) == 0:
            msg = msg or f"{conteneur!r} est vide"
            raise self.failureException(msg)
    
    def assertBetween(self, valeur, minimum, maximum, msg=None):
        """Vérifie que valeur est entre min et max"""
        if not (minimum <= valeur <= maximum):
            msg = msg or f"{valeur} n'est pas entre {minimum} et {maximum}"
            raise self.failureException(msg)
    
    def assertDateEqual(self, date1, date2, msg=None):
        """Compare seulement la date (ignore l'heure)"""
        if date1.date() != date2.date():
            msg = msg or f"{date1.date()} != {date2.date()}"
            raise self.failureException(msg)
    
    def assertDictContainsSubset(self, subset, dictionary, msg=None):
        """Vérifie que dictionary contient toutes les clés/valeurs de subset"""
        for key, value in subset.items():
            if key not in dictionary:
                msg = msg or f"Clé '{key}' manquante"
                raise self.failureException(msg)
            if dictionary[key] != value:
                msg = msg or f"{dictionary[key]} != {value} pour clé '{key}'"
                raise self.failureException(msg)


# Utilisation:
class TestAvecAssertionsCustom(CustomAssertions, unittest.TestCase):
    
    def test_liste_vide(self):
        liste = []
        self.assertIsEmpty(liste)
    
    def test_age_valide(self):
        age = 25
        self.assertBetween(age, 18, 65)
    
    def test_dates(self):
        from datetime import datetime
        date1 = datetime(2024, 1, 15, 10, 30)
        date2 = datetime(2024, 1, 15, 14, 45)
        self.assertDateEqual(date1, date2)


[OK] ORGANISATION AVANCÉE DES TESTS

# === Structure recommandée ===

project/
├── src/
│   └── myapp/
│       ├── __init__.py
│       ├── models.py
│       ├── views.py
│       └── utils.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py              # Configuration partagée
│   ├── test_models.py
│   ├── test_views.py
│   ├── test_utils.py
│   ├── unit/                    # Tests unitaires
│   │   ├── __init__.py
│   │   └── test_functions.py
│   ├── integration/             # Tests d'intégration
│   │   ├── __init__.py
│   │   └── test_database.py
│   └── e2e/                     # Tests end-to-end
│       ├── __init__.py
│       └── test_workflows.py
└── setup.py


# === Base class commune ===

# tests/__init__.py
class BaseTestCase(unittest.TestCase):
    """Classe de base pour tous les tests"""
    
    @classmethod
    def setUpClass(cls):
        """Configuration commune à tous les tests"""
        cls.config = charger_config_test()
        cls.connexion_db = creer_connexion_test()
    
    @classmethod
    def tearDownClass(cls):
        """Nettoyage commun"""
        cls.connexion_db.close()
    
    def assertStatusOk(self, response):
        """Assertion personnalisée commune"""
        self.assertEqual(response.status_code, 200)


# Utilisation:
class TestModels(BaseTestCase):
    def test_creation_user(self):
        # Hérite de setUpClass et assertStatusOk
        pass


# === Catégories de tests avec décorateurs ===

def slow_test(func):
    """Marquer un test comme lent"""
    func.slow = True
    return func

def requires_network(func):
    """Marquer un test nécessitant le réseau"""
    func.requires_network = True
    return func

class TestCategories(unittest.TestCase):
    
    def test_rapide(self):
        """Test rapide standard"""
        pass
    
    @slow_test
    def test_lent(self):
        """Test lent (peut être skippé)"""
        time.sleep(5)
    
    @requires_network
    def test_api(self):
        """Test nécessitant connexion"""
        response = requests.get('https://api.example.com')

# Exécuter seulement tests rapides:
# python -m unittest discover -k "not slow"


[OK] RAPPORTS ET SORTIE PERSONNALISÉE

# === TestResult personnalisé ===

class VerboseTestResult(unittest.TextTestResult):
    """Résultat de test avec output détaillé"""
    
    def startTest(self, test):
        super().startTest(test)
        self.stream.write(f"\n[BLACK_RIGHT-POINTING_TRIANGLE] Démarrage: {test.id()}")
        self.stream.flush()
    
    def addSuccess(self, test):
        super().addSuccess(test)
        self.stream.write(" [OK]")
        self.stream.flush()
    
    def addFailure(self, test, err):
        super().addFailure(test, err)
        self.stream.write(" [X] ÉCHEC")
        self.stream.flush()
    
    def addError(self, test, err):
        super().addError(test, err)
        self.stream.write(" [ATTENTION] ERREUR")
        self.stream.flush()

class VerboseTestRunner(unittest.TextTestRunner):
    resultclass = VerboseTestResult


# === HTML Report ===

import html

class HTMLTestResult(unittest.TestResult):
    """Génère un rapport HTML"""
    
    def __init__(self):
        super().__init__()
        self.test_results = []
    
    def startTest(self, test):
        super().startTest(test)
        self._test_start = time.time()
    
    def addSuccess(self, test):
        super().addSuccess(test)
        duree = time.time() - self._test_start
        self.test_results.append({
            'name': test.id(),
            'status': 'success',
            'duration': duree,
            'message': ''
        })
    
    def addFailure(self, test, err):
        super().addFailure(test, err)
        duree = time.time() - self._test_start
        self.test_results.append({
            'name': test.id(),
            'status': 'failure',
            'duration': duree,
            'message': self._exc_info_to_string(err, test)
        })
    
    def generate_html(self, filename='test_report.html'):
        """Générer rapport HTML"""
        html_content = f"""
        <!DOCTYPE html>
        <html>
        <head>
            <title>Test Report</title>
            <style>
                body {{ font-family: Arial, sans-serif; }}
                .success {{ color: green; }}
                .failure {{ color: red; }}
                table {{ border-collapse: collapse; width: 100%; }}
                th, td {{ border: 1px solid #ddd; padding: 8px; }}
            </style>
        </head>
        <body>
            <h1>Test Report</h1>
            <p>Total: {len(self.test_results)}</p>
            <p>Success: {sum(1 for r in self.test_results if r['status'] == 'success')}</p>
            <p>Failures: {sum(1 for r in self.test_results if r['status'] == 'failure')}</p>
            <table>
                <tr>
                    <th>Test</th>
                    <th>Status</th>
                    <th>Duration</th>
                    <th>Message</th>
                </tr>
        """
        
        for result in self.test_results:
            status_class = result['status']
            html_content += f"""
                <tr>
                    <td>{html.escape(result['name'])}</td>
                    <td class="{status_class}">{result['status']}</td>
                    <td>{result['duration']:.3f}s</td>
                    <td>{html.escape(result['message'])}</td>
                </tr>
            """
        
        html_content += """
            </table>
        </body>
        </html>
        """
        
        with open(filename, 'w') as f:
            f.write(html_content)


[OK] INTÉGRATION AVEC CI/CD

# === Configuration GitHub Actions ===

# .github/workflows/tests.yml
"""
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9, 3.10, 3.11]
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install -r requirements-dev.txt
    
    - name: Run tests
      run: |
        python -m unittest discover -s tests -p "test_*.py" -v
    
    - name: Generate coverage
      run: |
        pip install coverage
        coverage run -m unittest discover
        coverage report
        coverage html
"""


# === Configuration GitLab CI ===

# .gitlab-ci.yml
"""
image: python:3.11

stages:
  - test
  - report

test:
  stage: test
  script:
    - pip install -r requirements.txt
    - python -m unittest discover -v
  
test_avec_coverage:
  stage: test
  script:
    - pip install coverage
    - coverage run -m unittest discover
    - coverage report
    - coverage html
  artifacts:
    paths:
      - htmlcov/
"""


# === Script de test local