<jupyter_start><jupyter_text>Tester avec [pytest](https://docs.pytest.org/en/latest/) **Copier ce fichier dans votre espace personnel** Pour pouvoir utiliser `pytest` à l'intérieur d'un notebook (exécuter)<jupyter_code>import pytest
import ipytest
ipytest.autoconfig()<jupyter_output><empty_output><jupyter_text>*Utiliser la table des matières pour ouvrir/fermer chaque section.*  Introduction  Pourquoi écrire des tests? * Qui a envie de faire des **tests à la main** (print, suite d'interactions...) lorsque ça va mal?* Lorsque vous venez à bout d'un bogue -*bug*-, les tests sont une façon de s'assurer que vous n'en avez pas introduit d'autres par la même occasion* Si vous avez des **prérequis clairs**, vous pouvez vérifier qu'ils sont bien respectés en réalisant **un test pour chacun d'eux*** Vous n'aurez pas peur au moment de la réorganisation du code -**refactoring**-* Les tests *documentent l'organisation* de votre code - ils montrent aux autres codeurs des cas d'utilisation -*use case*- de votre implémentation* Cette liste est sans fin...  [Développement dirigé par les tests - Test-driven development](https://en.wikipedia.org/wiki/Test-driven_development) aka TDD En bref, l'idée de base du TDD - développement dirigé par les tests - est d'**écrire les tests avant même de réaliser l'implémentation effective**.Le bénéfice le plus important, probablement, c'est que le développeur porte toute son attention à écrire des tests qui exprime ce que le programme est censé faire.Lorsqu'on procède dans l'autre sens - tests écris après l'implémentation - il y a de fortes chances qu'ils ne soient qu'une paraphrase de la logique déjà utilisée (et probablement mal ficelée) pour l'implémentation actuelle.Les tests sont «des citoyens de première classe» dans le **développement moderne et agile d'applications**, voila pourquoi il est si important de commencer à **penser TDD** pendant votre apprentissage de Python. La manière de travailler du TDD peut se résumer comme suit:1. **Ajouter un scénario de test(s)** (*test case*) pour chaque modification / nouvelle fonctionnalité / résolution de bug que vous vous apprêter à entreprendre,2. Faire tourner tous les tests et vérifier que *le nouveau* **échoue**,3. **Implémenter** les changements requis,4. Faire de nouveau *tourner tous les tests* et vérifier que tout se passe bien (y compris le ou les nouveaux)5. **Réorganiser** le code (*refactoring*)  Scénario de test avec `pytest` Supposons disposer d'une fonction nommée `somme_de_trois_nombres` pour laquelle nous souhaitons réaliser un test.<jupyter_code># Cela pourrait être, par exemple, dans un fichier implementation.py
def somme_de_trois_nombres(nb1, nb2, nb3):
    return nb1 + nb2 + nb3<jupyter_output><empty_output><jupyter_text>Les scénarios de test de `pytest` sont en réalité très similaires à ce que vous avez déjà vu dans les exercices.La plupart des exercices sont structurés comme des scénarios de test en divisant chaque exercice en trois cellules:1. Définition des variables à utiliser dans les tests,2. Votre implémentation (À ton tour!),3. Vérification que votre implémentation respecte l'énoncé en utilisant des assertions (`assert `).Voir l'exemple de scénario de test ci-dessous pour observer les similitudes entre l'organisation des exercices et la structure habituel des scénarios de test.<jupyter_code>%%run_pytest[clean]
# Mention spéciale pour un notebook 
# à utiliser au tout début de la cellule qui contient un ou plusieurs tests.
# C'est seulement requis pour faire fonctionner pytest dans les notebooks Jupyter.

# Cela pourrait se situer dans un fichier test_implementation.py
def test_somme_de_trois_nombres():
    # 1. Définir les variables utilisées dans le test
    nb1 = 2
    nb2 = 3
    nb3 = 5
    
    # 2. Appeler la fonction à tester
    resultat = somme_de_trois_nombres(nb1, nb2, nb3)
    
    # 3. Vérifier que cela produit le résultat voulu
    assert resultat == 10<jupyter_output><empty_output><jupyter_text>Maintenant changer la ligne `assert resultat == 10` de façon à faire échouer l'assertion afin de voir à quoi ressemble un test qui échoue. Un test donné peut bien sûr contenir plusieurs assertions.  Exercice - Créer votre premier scénario de test Voici l'implémentation de la fonction `obtenir_les_multiples_de_cinq`. Votre mission est de créer un scénario de test pour vérifier que cette fonction fait bien ce qu'elle est censée faire.<jupyter_code>def obtenir_les_multiples_de_cinq(nombres):
    '''Retourne la liste des nombres qui sont divisibles par cinq dans la liste fournie en argument'''
    resultat = []
    for nb in nombres:
        if not nb % 5:
            resultat.append(nb)

    return resultat
%%run_pytest[clean]

def test_obtenir_les_multiples_de_cinq():
    # Votre implémentation ici<jupyter_output><empty_output><jupyter_text>Solution<jupyter_code>%%run_pytest[clean]

def test_obtenir_les_multiples_de_cinq():
    # 1. Définir variables ...
    nbs1 = [0, 5, 10, 15]
    nbs2 = [2, 15, -28, -10125]
    
    # 2. Appeler la fonction
    res0 = obtenir_les_multiples_de_cinqs([])
    res1 = obtenir_les_multiples_de_cinq(nbs1)
    res2 = obtenir_les_multiples_de_cinq(nbs2)
    
    # 3. quelques assertions
    assert res0 = []
    assert res1 == nbs1
    assert res2 == [nbs2[1], nbs2[-1]]<jupyter_output><empty_output><jupyter_text>Réutilisation - [`@pytest.fixture`](https://docs.pytest.org/en/latest/fixture.htmlpytest-fixtures-explicit-modular-scalable) Supposons disposer de l'implémentation d'une classe `Personne` que nous souhaitons tester.<jupyter_code># Cela pourrait se trouver dans un fichier personne.py par exemple
class Personne:
    def __init__(self, prenom, nom, age):
        self.prenom = prenom
        self.nom = nom
        self.age = age
    
    @property
    def nom_complet(self):
        return f'{self.prenom} {self.nom}'
    
    @property
    def comme_dictionnaire(self):
        return {'nom': self.nom_complet, 'age': self.age}
        
    def augmenter_age(self, annees):
        if annees < 0:
            raise ValueError('Je ne peux pas rajeunir les gens :(')
        self.age += annees<jupyter_output><empty_output><jupyter_text>Vous pouvez facilement **réutiliser du code de test** en utilisant les «**fixtures**» de pytest.Si vous placez vos fixtures dans un fichier [_conftest.py_](https://docs.pytest.org/en/latest/fixture.htmlconftest-py-sharing-fixture-functions), elles seront disponibles pour tous vos scénarios de test.En général, le fichier _conftest.py_ se situe à la racine d'un répertoire _tests_, qui comme son nom l'indique va grouper tous vos fichiers de tests.<jupyter_code># Cela serait soit dans conftest.py soit dans test_personne.py
@pytest.fixture()
def personne_par_defaut():
    personne = Personne(prenom='John', nom='Doe', age=82)
    return personne<jupyter_output><empty_output><jupyter_text>Dès lors, vous pouvez utiliser la fixture `personne_par_defaut` dans les scénarios de tests effectifs.<jupyter_code>%%run_pytest[clean]

# Cela se trouverait dans le fichier test_personne.py
def test_nom_complet(personne_par_defaut): # Note: nous utilisons la fixture comme un argument du test
    resultat = personne_par_defaut.nom_complet
    assert resultat == '<NAME>'
    
    
def test_comme_dictionnaire(personne_par_defaut):
    attendu = {'nom': '<NAME>', 'age': 82}
    resultat = personne_par_defaut.comme_dictionnaire
    assert resultat == attendu
    
    
def test_augmenter_age(personne_par_defaut):
    personne_par_defaut.augmenter_age(1)
    assert personne_par_defaut.age == 83
    
    personne_par_defaut.augmenter_age(10)
    assert personne_par_defaut.age == 93
    
    
def test_augmenter_age_avec_nombre_negatif(personne_par_defaut):
    with pytest.raises(ValueError):
        personne_par_defaut.augmenter_age(-1)<jupyter_output><empty_output><jupyter_text>En utilisant une fixture, nous pouvons utiliser la même `personne_par_defaut` pour tout nos scénarios de test!Dans le `test_augmenter_age_avec_nombre_negatif` nous avons utilisé [`pytest.raises`](https://docs.pytest.org/en/latest/assert.htmlassertions-about-expected-exceptions) pour vérifier qu'une exception a bien été levée.   Exercice - Finaliser des scénarios de tests La partie test de l'implémentation de `ListeTaches` est incomplète. Compléter les portions `____` des tests.<jupyter_code>class TacheNonTrouvee(Exception):
    pass


class ListeTaches:
    def __init__(self):
        self._tache = {}
        self._fait = {}
        self._compteur_tache = 1

    @property
    def taches(self):
        return self._tache

    @property
    def taches_faites(self):
        return self._fait

    def ajouter(self, tache):
        self._tache[self._compteur_tache] = tache
        self._compteur_tache += 1

    def achever(self, nombre):
        if nombre not in self._tache:
            raise TacheNonTrouvee(f"{nombre} n'est pas dans la liste")

        tache = self._tache.pop(nombre)
        self._fait[nombre] = tache

    def supprimer(self, nombre):
        if nombre not in self._tache:
            raise TacheNonTrouvee(f"{nombre} n'est pas dans la liste")

        del self._tache[nombre]<jupyter_output><empty_output><jupyter_text>Finaliser les tests pour `ListeTaches`.<jupyter_code>%%run_pytest[clean]


@pytest.____
def liste_taches():
    lt = ListeTaches()
    lt.ajouter('acheter du lait')
    lt.ajouter('sortir le chien')
    lt.ajouter('apprendre les fixtures de pytest')
    ____ ____


def test_taches_property(liste_taches):
    a_faire = liste_taches.taches
    assert a_faire == {
        1: 'acheter du lait',
        2: 'sortir le chien',
        3: 'apprendre les fixtures de pytest'
    }


def test_ajouter(____):
    liste_taches.ajouter('Vérifier dans la doc de pytest')
    a_faire = liste_taches.taches
    assert a_faire[4] == ____


def test_achever(liste_taches):
    # S'assurer qu'aucune tache n'a encore été faite
    assert not liste_taches.taches_faites

    liste_taches.achever(3)
    fait = liste_taches.____
    a_faire = liste_taches.____
    assert fait[3] == 'apprendre les fixtures de pytest'
    assert 3 not in ____


def test_achever_avec_no_tache_inconnu(liste_taches):
    # Voila comment tester qu'une certaine exception est bien levée
    with pytest.raises(TacheNonTrouvee):
        liste_taches.achever(10)


def test_supprimer(liste_taches):
    liste_taches.supprimer(1)
    fait = liste_taches.taches_faites
    a_faire = liste_taches.taches

    assert 1 not in ____
    # S'assurer que la tache n'a pas été mise dans fait
    ____ not fait


def test_supprimer_avec_no_tache_inconnu(liste_taches):
    with pytest.____(____):
        liste_taches.supprimer(12)<jupyter_output><empty_output><jupyter_text>Solution<jupyter_code>%%run_pytest[clean]


@pytest.fixture()
def liste_taches():
    lt = ListeTaches()
    lt.ajouter('acheter du lait')
    lt.ajouter('sortir le chien')
    lt.ajouter('apprendre les fixtures de pytest')
    return lt


def test_taches_property(liste_taches):
    a_faire = liste_taches.taches
    assert a_faire == {
        1: 'acheter du lait',
        2: 'sortir le chien',
        3: 'apprendre les fixtures de pytest',
    }


def test_ajouter(liste_taches):
    liste_taches.ajouter('Vérifier dans la doc de pytest')
    a_faire = liste_taches.taches
    assert a_faire[4] == 'Vérifier dans la doc de pytest'


def test_achever(liste_taches):
    # S'assurer qu'aucune tache n'a encore été faite
    assert not liste_taches.taches_faites

    liste_taches.achever(3)
    fait = liste_taches.taches_faites
    a_faire = liste_taches.taches
    assert fait[3] == 'apprendre les fixtures de pytest'
    assert 3 not in a_faire


def test_achever_avec_no_tache_inconnu(liste_taches):
    # Voila comment tester qu'une certaine exception est bien levée
    with pytest.raises(TacheNonTrouvee):
        liste_taches.achever(10)


def test_supprimer(liste_taches):
    liste_taches.supprimer(1)
    fait = liste_taches.taches_faites
    a_faire = liste_taches.taches

    assert 1 not in a_faire
    # S'assurer que la tache n'a pas été mise dans fait
    assert 1 not in fait


def test_supprimer_avec_no_tache_inconnu(liste_taches):
    with pytest.raises(TacheNonTrouvee):
        liste_taches.supprimer(12)<jupyter_output><empty_output><jupyter_text>Factorisation - [`@pytest.mark.parametrize`](https://docs.pytest.org/en/latest/parametrize.htmlpytest-mark-parametrize-parametrizing-test-functions) Parfois vous voulez tester la même fonctionnalité avec de **multiples entrées**. `pytest.mark.parametrize` est votre solution pour définir de multiples entrées avec les sorties attendues. Considérons l'implémentation suivante de la fonction `remplacer_noms`.<jupyter_code># Par exemple dans string_manipulate.py
def remplacer_noms(chaine_originale, nouveau_nom):
    """Remplace les noms (qui débutent par une majuscule) de la chaine_originale par nouveau_nom"""
    mots = chaine_originale.split()
    mots_manipulees = [nouveau_nom if mot.istitle() else mot for mot in mots]
    return ' '.join(mots_manipulees)<jupyter_output><empty_output><jupyter_text>Nous pouvons tester cette fonction avec plusieurs entrées en utilisant l'annotation:```python@pytest.mark.parametrize("par1, par2, res_attendu", [(v1, v2, v3), (ov1, ov2, ov3), ...])def test_fn(par1, par2, res_attendu):    ...```Voici un exemple:<jupyter_code>%%run_pytest[clean]

# Cela pourrait être dans votre module de test
@pytest.mark.parametrize("orig, nom, attendu", [
        ('je suis Lisa', '<NAME>', 'je suis <NAME>'),
        ('comment vont Anne et Bob', 'John', 'comment vont John et John'),
        ('pas de nom ici', '<NAME>', 'pas de nom ici'),
    ])
def test_remplacer_noms(orig, nom, attendu):
    resultat = remplacer_noms(orig, nom)
    assert resultat == attendu<jupyter_output><empty_output><jupyter_text>Exercice - Tester les [nombres de Fibonacci](https://en.wikipedia.org/wiki/Fibonacci_number) Implémenter un test pour la fonction `fibonacci`. Utiliser `pytest.mark.parametrize` et tester au moins avec les nombres: 0, 1, 2, 3, et 10. Vous pouvez trouver les résultats attendus et plus d'informations à propos des suites de Fibonacci [ici](https://en.wikipedia.org/wiki/Fibonacci_number).<jupyter_code>def fibonacci(nombre):
    if nombre in [0, 1]:
        return nombre
    return fibonacci(nombre - 1) + fibonacci(nombre - 2)
%%run_pytest[clean]

# À vous de jouer!<jupyter_output><empty_output><jupyter_text>Solution<jupyter_code>%%run_pytest[clean]

@pytest.mark.parametrize("nb,attendu",[(0,0),(1,1),(2,1),(3,2),(10,55)])
def test_fibonacci(nb, attendu):
    resultat = fibonacci(nb)
    assert resultat == attendu<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08954105951246084, 'fraction_numerical': 0.009464796404739207, 'mean_word_length': 4.364134404674945, 'pattern_counts': {'":': 0, '<': 67, '<?xml version=': 0, '>': 66, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 1, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '633230', 'license': 'CC0-1.0', 'n_tokens_mistral': 5267, 'n_tokens_neox': 5214, 'n_words': 1434, 'path': '0_Projet/4_tests_avec_pytest.ipynb', 'repo_name': 'efloti/cours-nsi-terminale', 'size': '14696'}